{
  "openapi": "3.1.0",
  "info": {
    "title": "Reporting Ninja API",
    "version": "1.5.0",
    "description": "Reporting Ninja public REST API including [OpenAPI spec](/api/v1/openapi.json). Read your marketing data across all supported integrations -- paid social, paid search, organic social, web analytics, search console, CRM, email, ecommerce, retail media and AI advertising -- and build, schedule and share the reports made of it. All endpoints are POST, require Bearer authentication and return JSON envelopes following the schemas below.\n\n## Error envelope and `error_code`\n\nEvery error response carries a machine-readable `error_code` (UPPER_SNAKE_CASE) in addition to a human-readable `message`. Clients should switch on `error_code` for error handling instead of parsing the message text. See `components/schemas/ErrorEnvelope` for the full enum. A validation failure also carries `details`, whose three known shapes are described in that same schema.\n\n## HTTP status codes\n\nThe two families of endpoints answer differently, and a client must know which one it is calling.\n\n**Framework errors apply to every endpoint** and always carry a real HTTP status:\n\n- **HTTP 400** when the `Content-Type` is not `application/json`, when the body cannot be read, or when the body is not valid JSON (`error_code: INVALID_REQUEST`). A client that treated any non-200 as a transport failure should not retry a 400: the body carries the reason.\n- **HTTP 401** when the Bearer token is missing or invalid (`error_code: AUTH_INVALID`).\n- **HTTP 413** when the request body exceeds 1 MB (`error_code: PAYLOAD_TOO_LARGE`). The cap is applied before the key is read, so a 413 is answered without authenticating.\n- **HTTP 429** when a rate limit is exceeded, with a `Retry-After` header (`error_code: RATE_LIMITED`, or `EMAIL_QUOTA_REACHED` on the calls that send email).\n- **HTTP 500** for an unclassified server-side error (`error_code: INTERNAL`).\n\n**The data endpoints** (`/integrations`, `/integrations/detail`, `/connections`, `/fields`, `/query`, `/query/async`, `/query/status`) answer their own business errors with **HTTP 200** and `envelope.status = \"error\"`: a missing field, an unknown integration, an invalid filter or a provider refusal all arrive inside a 200. For these endpoints the HTTP status alone is NOT sufficient to discriminate success from failure -- always read `envelope.status` and `envelope.error_code`.\n\n**The report endpoints** (everything under `/reports`, `/templates`, `/widgets`, `/sections`, `/catalog`, `/schedules`, `/portal-views`, `/users`, and `/capabilities`) answer with the real status: 400 for a body that does not validate, 403 when write access is off or the plan is full, 404 for a key this account does not own, 409 for a conflict, 422 when a widget was run and refused, 502 when a notification email failed, 503 when writes are paused. The envelope is always in the body, whatever the status.\n\n## Write access\n\nWriting is opt-in per account. The account owner enables it in the REST API tab of the app; until then every write answers **HTTP 403** with `error_code: WRITE_ACCESS_DISABLED`. Reading is unaffected. When writes are paused for everyone, a write answers **HTTP 503** with `error_code: WRITE_TEMPORARILY_UNAVAILABLE` and is worth retrying later. The current state of the calling account is published as `capabilities.account.write_access_enabled`.\n\n## Write guarantees\n\nEvery write endpoint accepts the same five control fields, and `/v1/capabilities` publishes their exact ceilings.\n\n- `dry_run` (boolean): runs the whole validation and answers what WOULD happen without writing anything. The answer is HTTP 200 with `data.dry_run: true` and `data.valid: true` plus the preview, or `data.valid: false` with the list of problems. A dry run never consumes an idempotency key and never consumes a confirmation token.\n- `idempotency_key` (string, `[A-Za-z0-9_.-]{1,128}`): makes a retry safe. The answer of the first call is replayed for 24 hours to any later call that sends the same key and the same body; the replay carries `meta.replayed: true` and `meta.replayed_from_request_id`. The same key with a different body is **HTTP 409** `IDEMPOTENCY_CONFLICT`, and an identical call still running is **HTTP 409** `IDEMPOTENCY_IN_PROGRESS` with a `Retry-After`.\n- `confirmation_token` (string): the endpoints that delete, that email or that change who can see a report answer the first call with HTTP 200, `data.confirmation_required: true`, a `confirmation_token`, an `expires_at` and the preview, without writing. Sending the same body again with that token performs the write. A token is valid for 10 minutes, is good for one use only, and belongs to one account, one endpoint and one body: anything else is **HTTP 409** `CONFIRMATION_EXPIRED` or `CONFIRMATION_MISMATCH`.\n- `if_version` (number): optimistic lock over the structure of a report's sections, accepted by the endpoints whose row in `capabilities.endpoints[]` says `supports_if_version: true` -- the ones that add, copy, delete or reorder a section, and the ones that write a widget into one. A mismatch is **HTTP 409** `VERSION_MISMATCH` with `details` carrying `expected` and `actual`. Sending it to any other endpoint is **HTTP 400** `INVALID_VALUE` rather than being ignored.\n- `verify_data` (boolean): accepted only by `/widgets/create`. It runs the widget against the provider before writing it, waits up to 45 seconds and publishes the verdict as `data.verification`. A `failure` verdict refuses the write with **HTTP 422** `WIDGET_EXECUTION_FAILED`.\n\n## Deleting a report\n\n`/v1/reports/delete` puts the report in the trash instead of removing it. It stays there for 7 days: it disappears from `/v1/reports/list`, from `/v1/templates/list` and from the plan counts, its key stops resolving anywhere (every endpoint that takes a `report_key` answers **HTTP 409** `REPORT_IN_TRASH`, with `trashed_at`, `restorable_until` and `restore` in `details`), and its public share link stops working. `/v1/reports/trash` lists what is in there and `/v1/reports/restore` brings a report back with the type it had before, subject to the plan having room for it again. The scheduled deliveries of the report are deleted when it goes to the trash and a restore does NOT recreate them: create them again with `/v1/schedules/create`. A report in the trash still counts as being in use, so a brand template, a language file, a filter or a calculated metric it references cannot be deleted while it is there.\n\nSending `permanent: true` skips the trash and deletes the report for good, and is also how a report already in the trash is removed before its 7 days are up. That has no undo, which is why both forms ask for a confirmation first.\n\n## Rate limits\n\nReads share a per-API-key budget of **300 requests/minute AND 5000 requests/day**; writes have a separate per-account budget of **60 requests/minute AND 1000 requests/day**. Both limits of a budget apply simultaneously, and exceeding either is **HTTP 429** with a `Retry-After` header (seconds until the more restrictive window resets).\n\nThe `X-RateLimit-*` headers are present on every response that got past authentication, and always report the **read** budget; only the 429 of a write reports the write budget instead. A 400 about the body, a 401 and a 413 are answered before the budget is read and carry no `X-RateLimit-*` header at all.\n\n- `X-RateLimit-Limit`: per-minute cap\n- `X-RateLimit-Remaining`: requests remaining in the current minute window\n- `X-RateLimit-Reset`: unix epoch seconds when the minute window resets\n- `X-RateLimit-Limit-Day`: per-day cap\n- `X-RateLimit-Remaining-Day`: requests remaining today\n- `X-RateLimit-Reset-Day`: unix epoch seconds when the day window resets\n\nSeparately, the calls that send email (creating a schedule, sending one now, notifying portal users, creating a client user) share a budget of 50 per account per day. Exceeding it is **HTTP 429** with `error_code: EMAIL_QUOTA_REACHED` and a `Retry-After` pointing at the next UTC midnight.",
    "contact": {
      "name": "Reporting Ninja support",
      "url": "https://www.reportingninja.com/"
    }
  },
  "servers": [
    {
      "url": "https://api.reportingninja.com/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Integrations",
      "description": "List available integrations and inspect their data views, settings and filter catalog."
    },
    {
      "name": "Connections",
      "description": "List the consumer's authorized connections (e.g. Meta accounts) per integration."
    },
    {
      "name": "Fields",
      "description": "List the dimensions and metrics available for a given connection."
    },
    {
      "name": "Query",
      "description": "Execute data queries synchronously or asynchronously."
    },
    {
      "name": "Reports",
      "description": "Create, read, extend and delete the reports and templates of the account."
    },
    {
      "name": "Widgets",
      "description": "Read, add, rewrite, restore and remove the widgets of a section of a report."
    },
    {
      "name": "Sections",
      "description": "Remove a section of a report with the widgets it holds."
    },
    {
      "name": "Catalog",
      "description": "Browse the published templates a report can be created from."
    },
    {
      "name": "Schedules",
      "description": "Manage the scheduled deliveries of a report by email."
    },
    {
      "name": "Portal Views",
      "description": "Manage what each client user sees in the client portal."
    },
    {
      "name": "Users",
      "description": "Manage the client users of the account."
    },
    {
      "name": "Capabilities",
      "description": "Read the ceilings, vocabularies and guarantees this version of the API publishes."
    },
    {
      "name": "Filters",
      "description": "Read the saved filters of the account and the conditions each one carries."
    },
    {
      "name": "Calculated Metrics",
      "description": "Read the metrics the account defined itself, of the three kinds the product has."
    },
    {
      "name": "Brand Templates",
      "description": "Read the brands a report can be printed with."
    },
    {
      "name": "Language Files",
      "description": "Read the language files a report can be printed with."
    },
    {
      "name": "Email Templates",
      "description": "Read the email templates a scheduled delivery can be sent with."
    },
    {
      "name": "Snapshots",
      "description": "The PDF a report files away, asked for by hand or taken with a scheduled delivery."
    }
  ],
  "paths": {
    "/integrations": {
      "post": {
        "operationId": "listIntegrations",
        "tags": [
          "Integrations"
        ],
        "summary": "List available integrations",
        "description": "Returns the set of integrations exposed by the public API, with the category of each one, whether its field list is account specific and whether its data can be read through the query endpoints. The catalog grows incrementally; check this endpoint to discover which integrations are available before calling /integrations/detail, /connections, /fields or /query.",
        "x-mcp-description": "Use this tool to discover which marketing data sources (integration_ids) are available before calling /connections, /fields or /query. The response is a stable list of id + name; it does not include the full configuration of each integration. To get that, call /integrations/detail.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmptyBody"
              },
              "example": {}
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer of the call, or a business error envelope. This endpoint reports its own errors with HTTP 200 and status=error: read envelope.status before reading data.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/IntegrationsListResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    }
                  ]
                },
                "examples": {
                  "ok": {
                    "summary": "The catalog as the endpoint answers it, with the category, the custom fields flag and the queryable flag of every integration.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "facebook_ads",
                            "name": "Meta Ads",
                            "category": "paid_social",
                            "supports_custom_fields_per_account": true,
                            "queryable": true
                          },
                          {
                            "id": "google_ads",
                            "name": "Google Ads",
                            "category": "paid_search",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "ga4",
                            "name": "Google Analytics 4",
                            "category": "analytics",
                            "supports_custom_fields_per_account": true,
                            "queryable": true
                          },
                          {
                            "id": "microsoft_ads",
                            "name": "Microsoft Ads",
                            "category": "paid_search",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "youtube",
                            "name": "YouTube",
                            "category": "video_analytics",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "google_search_console",
                            "name": "Google Search Console",
                            "category": "seo",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "google_business_profile",
                            "name": "Google Business Profile",
                            "category": "local_seo",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "facebook_insights",
                            "name": "Facebook Insights",
                            "category": "organic_social",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "instagram_insights",
                            "name": "Instagram Insights",
                            "category": "organic_social",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "linkedin_ads",
                            "name": "LinkedIn Ads",
                            "category": "paid_social",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "linkedin_pages",
                            "name": "LinkedIn Pages",
                            "category": "organic_social",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "tiktok_ads",
                            "name": "TikTok Ads",
                            "category": "paid_social",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "tiktok_organic",
                            "name": "TikTok Organic",
                            "category": "organic_social",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "pinterest_ads",
                            "name": "Pinterest Ads",
                            "category": "paid_social",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "pinterest_organic",
                            "name": "Pinterest Organic",
                            "category": "organic_social",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "snapchat_ads",
                            "name": "Snapchat Ads",
                            "category": "paid_social",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "reddit_ads",
                            "name": "Reddit Ads",
                            "category": "paid_social",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "twitter_analytics",
                            "name": "Twitter Analytics",
                            "category": "organic_social",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "twitter_ads",
                            "name": "Twitter Ads",
                            "category": "paid_social",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "mailchimp",
                            "name": "Mailchimp",
                            "category": "email",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "cross_platform",
                            "name": "Cross-platform",
                            "category": "advertising_aggregated",
                            "supports_custom_fields_per_account": true,
                            "queryable": true
                          },
                          {
                            "id": "hubspot",
                            "name": "HubSpot",
                            "category": "crm",
                            "supports_custom_fields_per_account": true,
                            "queryable": true
                          },
                          {
                            "id": "klaviyo",
                            "name": "Klaviyo",
                            "category": "email",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "shopify",
                            "name": "Shopify",
                            "category": "ecommerce",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "chatgpt_ads",
                            "name": "ChatGPT Ads",
                            "category": "paid_ai",
                            "supports_custom_fields_per_account": false,
                            "queryable": true
                          },
                          {
                            "id": "amazon_ads",
                            "name": "Amazon Ads",
                            "category": "retail_media",
                            "supports_custom_fields_per_account": false,
                            "queryable": false,
                            "not_queryable_reason": "Amazon Ads data cannot be queried through the API under the Amazon Ads API terms. Its reports and widgets can be managed through /v1/reports and /v1/widgets, and its fields and connections are listed as for any other integration."
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/integrations/detail": {
      "post": {
        "operationId": "getIntegrationDetail",
        "tags": [
          "Integrations"
        ],
        "summary": "Get integration details",
        "description": "Returns the full configuration for one integration: data views (if any), settings (e.g. attribution_window for facebook_ads) and the filter catalog (fields and allowed operators per field).",
        "x-mcp-description": "Use this tool when you need the precise filter field IDs, allowed operators and settings keys for an integration. The catalog returned here is the contract for /v1/query: field_ids you can request, filters you can apply, settings you can override. The response is the single source of truth for what is queryable.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IntegrationDetailRequest"
              },
              "example": {
                "integration_id": "facebook_ads"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer of the call, or a business error envelope. This endpoint reports its own errors with HTTP 200 and status=error: read envelope.status before reading data.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/IntegrationDetailResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    }
                  ]
                },
                "examples": {
                  "ok_facebook_ads": {
                    "summary": "facebook_ads detail (filter fields abridged: 2 of the 34 the endpoint returns)",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "facebook_ads",
                            "name": "Meta Ads",
                            "category": "paid_social",
                            "supports_custom_fields_per_account": true,
                            "data_views": null,
                            "settings": [
                              {
                                "id": "attribution_window",
                                "name": "Attribution window",
                                "recommended_value": "ATTRIBUTION_MODEL_VIEW_CLICK###VIEW_ATTRIBUTION_WINDOW_1D###CLICK_ATTRIBUTION_WINDOW_7D",
                                "options": [
                                  {
                                    "value": "ATTRIBUTION_MODEL_CLICK###VIEW_ATTRIBUTION_WINDOW_1D###CLICK_ATTRIBUTION_WINDOW_7D",
                                    "label": "7 days after click"
                                  },
                                  {
                                    "value": "ATTRIBUTION_MODEL_CLICK###VIEW_ATTRIBUTION_WINDOW_1D###CLICK_ATTRIBUTION_WINDOW_1D",
                                    "label": "1 day after click"
                                  },
                                  {
                                    "value": "ATTRIBUTION_MODEL_VIEW_CLICK###VIEW_ATTRIBUTION_WINDOW_1D###CLICK_ATTRIBUTION_WINDOW_7D",
                                    "label": "1 day after view + 7 days after click"
                                  },
                                  {
                                    "value": "ATTRIBUTION_MODEL_VIEW_CLICK###VIEW_ATTRIBUTION_WINDOW_1D###CLICK_ATTRIBUTION_WINDOW_1D",
                                    "label": "1 day after view + 1 day after click"
                                  },
                                  {
                                    "value": "ATTRIBUTION_MODEL_VIEW_CLICK###VIEW_ATTRIBUTION_WINDOW_7D###CLICK_ATTRIBUTION_WINDOW_7D",
                                    "label": "7 days after view + 7 days after click"
                                  },
                                  {
                                    "value": "ATTRIBUTION_MODEL_VIEW_CLICK###VIEW_ATTRIBUTION_WINDOW_7D###CLICK_ATTRIBUTION_WINDOW_1D",
                                    "label": "7 days after view + 1 day after click"
                                  },
                                  {
                                    "value": "ATTRIBUTION_MODEL_VIEW###VIEW_ATTRIBUTION_WINDOW_7D###CLICK_ATTRIBUTION_WINDOW_7D",
                                    "label": "7 days after view"
                                  },
                                  {
                                    "value": "ATTRIBUTION_MODEL_VIEW###VIEW_ATTRIBUTION_WINDOW_1D###CLICK_ATTRIBUTION_WINDOW_7D",
                                    "label": "1 day after view"
                                  },
                                  {
                                    "value": "ATTRIBUTION_MODEL_VIEW###VIEW_SKAN_ATTRIBUTION###CLICK_ATTRIBUTION_WINDOW_7D",
                                    "label": "SKAdNetwork - View-through"
                                  },
                                  {
                                    "value": "ATTRIBUTION_MODEL_CLICK###VIEW_ATTRIBUTION_WINDOW_1D###CLICK_SKAN_ATTRIBUTION",
                                    "label": "SKAdNetwork - Click-through"
                                  },
                                  {
                                    "value": "ATTRIBUTION_MODEL_VIEW_CLICK###VIEW_ATTRIBUTION_WINDOW_1D###CLICK_ATTRIBUTION_WINDOW_28D",
                                    "label": "1 day after view + 28 days after click"
                                  },
                                  {
                                    "value": "ATTRIBUTION_MODEL_VIEW_CLICK###VIEW_ATTRIBUTION_WINDOW_7D###CLICK_ATTRIBUTION_WINDOW_28D",
                                    "label": "7 days after view + 28 days after click"
                                  },
                                  {
                                    "value": "ATTRIBUTION_MODEL_VIEW_CLICK###VIEW_ATTRIBUTION_WINDOW_28D###CLICK_ATTRIBUTION_WINDOW_28D",
                                    "label": "28 days after view + 28 days after click"
                                  },
                                  {
                                    "value": "ATTRIBUTION_MODEL_VIEW_CLICK###VIEW_ATTRIBUTION_WINDOW_28D###CLICK_ATTRIBUTION_WINDOW_7D",
                                    "label": "28 days after view + 7 days after click"
                                  },
                                  {
                                    "value": "ATTRIBUTION_MODEL_VIEW_CLICK###VIEW_ATTRIBUTION_WINDOW_28D###CLICK_ATTRIBUTION_WINDOW_1D",
                                    "label": "28 days after view + 1 day after click"
                                  },
                                  {
                                    "value": "ATTRIBUTION_MODEL_VIEW###VIEW_ATTRIBUTION_WINDOW_28D###CLICK_ATTRIBUTION_WINDOW_28D",
                                    "label": "28 days after view"
                                  },
                                  {
                                    "value": "ATTRIBUTION_MODEL_CLICK###VIEW_ATTRIBUTION_WINDOW_1D###CLICK_ATTRIBUTION_WINDOW_28D",
                                    "label": "28 days after click"
                                  }
                                ]
                              }
                            ],
                            "filters": {
                              "supported": true,
                              "combination": "and",
                              "fields": [
                                {
                                  "id": "campaign.name",
                                  "name": "Campaign name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ],
                                  "aliases": [
                                    "campaign_name"
                                  ]
                                },
                                {
                                  "id": "campaign.effective_status",
                                  "name": "Campaign status",
                                  "category": "status",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "ACTIVE",
                                    "PAUSED",
                                    "DELETED",
                                    "PENDING_REVIEW",
                                    "DISAPPROVED",
                                    "PREAPPROVED",
                                    "PENDING_BILLING_INFO",
                                    "CAMPAIGN_PAUSED",
                                    "ARCHIVED",
                                    "ADSET_PAUSED"
                                  ]
                                }
                              ]
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-16T10:30:00Z"
                      }
                    }
                  },
                  "ok_google_ads": {
                    "summary": "google_ads detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "google_ads",
                            "name": "Google Ads",
                            "category": "paid_search",
                            "supports_custom_fields_per_account": false,
                            "data_views": [
                              {
                                "id": "campaign",
                                "name": "Campaign"
                              },
                              {
                                "id": "ad_group",
                                "name": "Ad group"
                              },
                              {
                                "id": "keyword_view",
                                "name": "Keyword"
                              }
                            ],
                            "settings": null,
                            "filters": {
                              "supported": true,
                              "combination": "and",
                              "note": "`CONTAINS` and `NOT_CONTAINS` match the value as literal text anywhere in a string field, ignoring case; the value is the plain text (for example `Brand`) and needs no wildcards. They are different from `CONTAINS_ANY`, `CONTAINS_ALL` and `CONTAINS_NONE`, which apply to label fields only and take a label. `LIKE` and `NOT_LIKE` take the % wildcard as part of the value (`%` matches any sequence, `_` any single character), so `LIKE` with `%Brand%` is equivalent to `CONTAINS` with `Brand`; a `LIKE` or `NOT_LIKE` value that neither starts nor ends with `%` is rejected as an invalid filter. To match a literal `%`, `_`, `[` or `]` with `LIKE`, surround the character with square brackets (`[_]`). A `[` that is never closed makes Google Ads reject the value as an invalid filter. `LIKE` and `NOT_LIKE` ignore case; `EQUALS`, `NOT_EQUALS`, `IN` and `NOT_IN` compare the whole value and are case sensitive; `REGEXP_MATCH` and `NOT_REGEXP_MATCH` are case sensitive and match partially unless the expression is anchored with ^ or $. Filters on `metrics.cost_micros` and `metrics.average_cpc` take the value in the account currency, the same scale as the values returned in the response.",
                              "fields": [
                                {
                                  "id": "campaign.id",
                                  "name": "Campaign ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "IN",
                                    "NOT_IN"
                                  ]
                                },
                                {
                                  "id": "campaign.name",
                                  "name": "Campaign name",
                                  "category": "string",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "LIKE",
                                    "NOT_LIKE",
                                    "IN",
                                    "NOT_IN",
                                    "REGEXP_MATCH",
                                    "NOT_REGEXP_MATCH"
                                  ]
                                },
                                {
                                  "id": "campaign.status",
                                  "name": "Campaign status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ]
                                },
                                {
                                  "id": "campaign.labels",
                                  "name": "Campaign labels",
                                  "category": "label",
                                  "operators": [
                                    "CONTAINS_ANY",
                                    "CONTAINS_ALL",
                                    "CONTAINS_NONE"
                                  ]
                                },
                                {
                                  "id": "metrics.clicks",
                                  "name": "Clicks",
                                  "category": "metric",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "GREATER_THAN",
                                    "GREATER_THAN_OR_EQUAL",
                                    "LESS_THAN",
                                    "LESS_THAN_OR_EQUAL"
                                  ]
                                },
                                {
                                  "id": "metrics.cost_micros",
                                  "name": "Cost (in account currency)",
                                  "category": "metric",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "GREATER_THAN",
                                    "GREATER_THAN_OR_EQUAL",
                                    "LESS_THAN",
                                    "LESS_THAN_OR_EQUAL"
                                  ]
                                }
                              ]
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-16T10:30:00Z"
                      }
                    }
                  },
                  "ok_ga4": {
                    "summary": "ga4 detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "ga4",
                            "name": "Google Analytics 4",
                            "category": "analytics",
                            "supports_custom_fields_per_account": true,
                            "notes": [
                              "Metrics of the `percent` field_type are returned in 0-100 scale (raw GA4 API), not normalized to 0-1. Divide by 100 client-side if a 0-1 representation is needed.",
                              "The date, week and month dimensions change format with the rest of the query. When one of them is the only dimension requested (metrics do not count), date is returned as YYYY-MM-DD, week as a YYYY-MM-DD date and month as YYYY-MM. When the query has any other dimension, they are returned as GA4 sends them: date as YYYYMMDD, week as WW and month as MM, without the year. The other time dimensions (year, yearMonth, dateHour, isoYearIsoWeek, ...) are always returned as GA4 sends them."
                            ],
                            "data_views": null,
                            "settings": null,
                            "filters": {
                              "supported": true,
                              "combination": "and_or",
                              "note": "Filter fields are DYNAMIC: use /v1/fields to discover the fields available for a connection+account (predefined + per-property custom dimensions/metrics). Each filter MUST include `type_of_field` (\"dimension\" or \"metric\") and MAY include `combination` (\"AND\" by default, or \"OR\"). Dimension fields use dimension_operators; metric fields use metric_operators (see the two lists below). For IN_LIST / NOT_IN_LIST the value MAY be an array; individual values must NOT contain commas. For metric filters the value MUST be numeric. Current limitations: (a) all filters in a single request must share the same type_of_field (mixed dimension+metric not supported yet); (b) when 2 or more filters are sent, the combination of the second filter defines the combination of the whole group (combinations of other positions are ignored, including the combination of a single-filter request)."
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_microsoft_ads": {
                    "summary": "microsoft_ads detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "microsoft_ads",
                            "name": "Microsoft Ads",
                            "category": "paid_search",
                            "supports_custom_fields_per_account": false,
                            "data_views": [
                              {
                                "id": "account_performance",
                                "name": "Account performance"
                              },
                              {
                                "id": "campaign_performance",
                                "name": "Campaign performance"
                              },
                              {
                                "id": "ad_performance",
                                "name": "Ad performance"
                              },
                              {
                                "id": "ad_extension_detail",
                                "name": "Ad extension detail"
                              },
                              {
                                "id": "keyword_performance",
                                "name": "Keyword performance"
                              },
                              {
                                "id": "search_query_performance",
                                "name": "Search query performance"
                              },
                              {
                                "id": "geographic_performance",
                                "name": "Geographic performance"
                              },
                              {
                                "id": "age_gender_audience",
                                "name": "Age gender audience"
                              },
                              {
                                "id": "product_dimension_performance",
                                "name": "Product dimension performance"
                              },
                              {
                                "id": "call_detail",
                                "name": "Call detail"
                              },
                              {
                                "id": "goals_and_funnels",
                                "name": "Goals and funnels"
                              }
                            ],
                            "settings": null,
                            "filters": {
                              "supported": true,
                              "combination": "and",
                              "note": "Filter fields availability depends on the data_view. For each field, the `data_views` array declares the data_views where it can be used. Some fields have a closed list of allowed values (`allowed_values`); if present, the filter value MUST belong to this list (values are SCREAMING_SNAKE_CASE enum names, e.g. ACTIVE, SMART_PHONE, SEARCH -- not the display strings). IN_LIST accepts an array of values; CONTAINS / NOT_CONTAINS accept a scalar string. Current limitations: (a) only AND combination is supported (the `combination` attribute is NOT accepted on filters); (b) `type_of_field` must not be included in YouTube filter requests; all YouTube filter fields are dimensions; (c) IN_LIST on free-text array-of-string fields (Keyword, SearchQuery, Country, Language) is forwarded to the Bing Ads Reporting API but the API may silently ignore it or require an exact match against an internal format -- if you need strict filtering on those fields, validate the result row-by-row client-side.",
                              "fields": [
                                {
                                  "id": "AdDistribution",
                                  "name": "Ad distribution",
                                  "category": "enum",
                                  "operators": [
                                    "IN_LIST"
                                  ],
                                  "allowed_values": [
                                    "Search",
                                    "Audience",
                                    "Content"
                                  ],
                                  "data_views": [
                                    "account_performance",
                                    "campaign_performance",
                                    "ad_performance",
                                    "keyword_performance",
                                    "geographic_performance",
                                    "age_gender_audience",
                                    "goals_and_funnels"
                                  ]
                                },
                                {
                                  "id": "CampaignStatus",
                                  "name": "Campaign status",
                                  "category": "enum",
                                  "operators": [
                                    "IN_LIST"
                                  ],
                                  "allowed_values": [
                                    "Active",
                                    "Paused",
                                    "Deleted"
                                  ],
                                  "data_views": [
                                    "campaign_performance",
                                    "search_query_performance",
                                    "geographic_performance",
                                    "age_gender_audience"
                                  ]
                                },
                                {
                                  "id": "CampaignName",
                                  "name": "Campaign name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ],
                                  "data_views": [
                                    "campaign_performance",
                                    "ad_performance",
                                    "ad_extension_detail",
                                    "keyword_performance",
                                    "search_query_performance",
                                    "product_dimension_performance",
                                    "goals_and_funnels"
                                  ]
                                },
                                {
                                  "id": "Keyword",
                                  "name": "Keyword",
                                  "category": "string",
                                  "operators": [
                                    "IN_LIST"
                                  ],
                                  "data_views": [
                                    "keyword_performance"
                                  ]
                                }
                              ]
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_youtube": {
                    "summary": "youtube detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "youtube",
                            "name": "YouTube",
                            "category": "video_analytics",
                            "supports_custom_fields_per_account": false,
                            "notes": [
                              "Data freshness: the YouTube Analytics API publishes per-day stats 4-5 days after the actual date, so the most recent 4-5 days of any date_range typically return zero or partial values."
                            ],
                            "data_views": [
                              {
                                "id": "basic",
                                "name": "Basic stats"
                              },
                              {
                                "id": "lifetime",
                                "name": "Lifetime stats"
                              },
                              {
                                "id": "geographic",
                                "name": "User geography"
                              },
                              {
                                "id": "playback_details",
                                "name": "Playback details"
                              },
                              {
                                "id": "playback_locations",
                                "name": "Playback locations"
                              },
                              {
                                "id": "traffic_sources",
                                "name": "Traffic sources"
                              },
                              {
                                "id": "device_and_os",
                                "name": "Device type and operating system"
                              },
                              {
                                "id": "viewer_demographics",
                                "name": "Viewer demographics"
                              },
                              {
                                "id": "content_sharing",
                                "name": "Content sharing"
                              },
                              {
                                "id": "audience_retention",
                                "name": "Audience retention"
                              },
                              {
                                "id": "top_videos",
                                "name": "Top videos"
                              },
                              {
                                "id": "playlist_basic",
                                "name": "Basic playlist stats"
                              },
                              {
                                "id": "top_playlists",
                                "name": "Top playlists"
                              }
                            ],
                            "settings": null,
                            "filters": {
                              "supported": true,
                              "combination": "and",
                              "note": "Filter fields availability depends on the data_view. For each field, the `data_views` array declares the data_views where it can be used. Four fields have a closed list of allowed values (`allowed_values`); if present, the filter value MUST belong to this list (case-sensitive). `country` accepts ISO 3166-1 alpha-2 country codes (a few legacy codes also accepted); invalid codes are rejected by the server before reaching the YouTube API. `videoId` and `playlistId` are YouTube resource IDs (free-text). Multi-value support: only `videoId` and `playlistId` accept multi-value IN_LIST (comma-separated OR semantics, native YouTube Analytics behavior for ID-list dimensions). All other fields (`country`, `subscribedStatus`, `liveOrOnDemand`, `youtubeProduct`, `insightTrafficSourceType`, `audienceType`) are single-value-only per filter -- YouTube Analytics API rejects multi-value on them with HTTP 400 \"Invalid value\". Use IN_LIST with a 1-element array or a scalar value for these. Only the IN_LIST operator is exposed (YouTube Analytics API only supports equality semantics). Individual filter values MUST NOT contain a comma (reserved as multi-value separator). `data_view: audience_retention` REQUIRES a filter on `videoId`. Current limitations: only AND combination is supported between distinct filter fields (the `combination` attribute is NOT accepted on filters); `type_of_field` must not be included in YouTube filter requests; all YouTube filter fields are dimensions.",
                              "fields": [
                                {
                                  "id": "country",
                                  "name": "Country",
                                  "category": "string",
                                  "operators": [
                                    "IN_LIST"
                                  ],
                                  "data_views": [
                                    "basic",
                                    "playback_details",
                                    "playback_locations",
                                    "traffic_sources",
                                    "device_and_os",
                                    "viewer_demographics",
                                    "content_sharing",
                                    "top_videos",
                                    "playlist_basic",
                                    "top_playlists"
                                  ]
                                },
                                {
                                  "id": "videoId",
                                  "name": "Video ID",
                                  "category": "id",
                                  "operators": [
                                    "IN_LIST"
                                  ],
                                  "data_views": [
                                    "basic",
                                    "geographic",
                                    "playback_details",
                                    "playback_locations",
                                    "traffic_sources",
                                    "device_and_os",
                                    "viewer_demographics",
                                    "content_sharing",
                                    "audience_retention"
                                  ]
                                },
                                {
                                  "id": "subscribedStatus",
                                  "name": "Subscribed status",
                                  "category": "enum",
                                  "operators": [
                                    "IN_LIST"
                                  ],
                                  "allowed_values": [
                                    "SUBSCRIBED",
                                    "UNSUBSCRIBED"
                                  ],
                                  "data_views": [
                                    "playback_details",
                                    "playback_locations",
                                    "traffic_sources",
                                    "device_and_os",
                                    "viewer_demographics",
                                    "content_sharing",
                                    "audience_retention",
                                    "playlist_basic",
                                    "top_playlists"
                                  ]
                                },
                                {
                                  "id": "insightTrafficSourceType",
                                  "name": "Traffic source type",
                                  "category": "enum",
                                  "operators": [
                                    "IN_LIST"
                                  ],
                                  "allowed_values": [
                                    "ADVERTISING",
                                    "EXT_URL",
                                    "RELATED_VIDEO",
                                    "SUBSCRIBER",
                                    "YT_CHANNEL",
                                    "YT_OTHER_PAGE",
                                    "YT_SEARCH"
                                  ],
                                  "data_views": [
                                    "traffic_sources"
                                  ]
                                }
                              ]
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_google_search_console": {
                    "summary": "google_search_console detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "google_search_console",
                            "name": "Google Search Console",
                            "category": "seo",
                            "supports_custom_fields_per_account": false,
                            "notes": [
                              "Data freshness: the Search Console API publishes per-day data 2-3 days after the actual date, so the most recent 2-3 days of any date_range typically return zero or partial values.",
                              "Each /v1/query request must include at least one metric and at most one dimension in the fields[] array (Search Console API constraint)."
                            ],
                            "data_views": null,
                            "settings": [
                              {
                                "id": "search_type",
                                "name": "Search type",
                                "recommended_value": "web",
                                "options": [
                                  {
                                    "value": "web",
                                    "label": "Web search"
                                  },
                                  {
                                    "value": "image",
                                    "label": "Image search"
                                  },
                                  {
                                    "value": "video",
                                    "label": "Video search"
                                  },
                                  {
                                    "value": "news",
                                    "label": "News search"
                                  }
                                ]
                              }
                            ],
                            "filters": {
                              "supported": true,
                              "combination": "and",
                              "note": "Filter fields: country, device, page, query (searchAppearance and date are NOT filterable). country accepts ISO 3166-1 alpha-3 LOWERCASE codes (e.g. usa, esp, fra) -- different from other integrations; invalid codes are rejected by the upstream Search Console API. device has a closed list of allowed_values. page and query are free-text. Each filter is a single value (the Search Console API does not support OR multi-value within a filter natively). Only AND combination is supported between distinct filter fields. type_of_field and combination must not be included in filter requests; all Search Console filter fields are dimensions and the combination is fixed to AND. MATCHES_REGEX and NOT_MATCHES_REGEX are available on page and query only, and take an RE2 expression: matching is partial unless anchored with ^ or $, and it does not distinguish uppercase from lowercase. Lookaheads, lookbehinds and backreferences are not supported and are rejected. Use MATCHES_REGEX to return several specific values in one request, for example ^(first term|second term)$.",
                              "fields": [
                                {
                                  "id": "country",
                                  "name": "Country",
                                  "category": "string",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ]
                                },
                                {
                                  "id": "device",
                                  "name": "Device",
                                  "category": "enum",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "allowed_values": [
                                    "DESKTOP",
                                    "MOBILE",
                                    "TABLET"
                                  ]
                                },
                                {
                                  "id": "page",
                                  "name": "Page",
                                  "category": "string",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "MATCHES_REGEX",
                                    "NOT_MATCHES_REGEX"
                                  ]
                                },
                                {
                                  "id": "query",
                                  "name": "Query",
                                  "category": "string",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "MATCHES_REGEX",
                                    "NOT_MATCHES_REGEX"
                                  ]
                                }
                              ]
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_google_business_profile": {
                    "summary": "google_business_profile detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "google_business_profile",
                            "name": "Google Business Profile",
                            "category": "local_seo",
                            "supports_custom_fields_per_account": false,
                            "notes": [
                              "Data freshness: the Business Profile Performance API publishes per-day stats 3-4 days after the actual date, so the most recent 3-4 days of any date_range typically return zero or partial values."
                            ],
                            "data_views": [
                              {
                                "id": "performance",
                                "name": "Performance"
                              },
                              {
                                "id": "search_terms",
                                "name": "Search terms"
                              },
                              {
                                "id": "reviews_timebound",
                                "name": "Reviews (timebound)"
                              },
                              {
                                "id": "reviews_lifetime",
                                "name": "Reviews (lifetime)"
                              }
                            ],
                            "settings": null,
                            "filters": {
                              "supported": false
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_facebook_insights": {
                    "summary": "facebook_insights detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "facebook_insights",
                            "name": "Facebook Insights",
                            "category": "organic_social",
                            "supports_custom_fields_per_account": false,
                            "notes": [
                              "Meta Graph API limitations apply unchanged: FANS metrics (page_fans, page_fans_city, ...) require period=day; post metrics with breakdown are omitted upstream."
                            ],
                            "data_views": [
                              {
                                "id": "page",
                                "name": "Page"
                              },
                              {
                                "id": "post",
                                "name": "Post"
                              },
                              {
                                "id": "post_aggregated",
                                "name": "Post (aggregated)"
                              },
                              {
                                "id": "reel",
                                "name": "Reel"
                              },
                              {
                                "id": "reel_aggregated",
                                "name": "Reel (aggregated)"
                              }
                            ],
                            "settings": null,
                            "filters": {
                              "supported": false
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_instagram_insights": {
                    "summary": "instagram_insights detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "instagram_insights",
                            "name": "Instagram Insights",
                            "category": "organic_social",
                            "supports_custom_fields_per_account": false,
                            "data_views": [
                              {
                                "id": "account",
                                "name": "Account"
                              },
                              {
                                "id": "posts",
                                "name": "Posts"
                              },
                              {
                                "id": "posts_aggregated",
                                "name": "Aggregated figures from posts in period"
                              }
                            ],
                            "settings": null,
                            "filters": {
                              "supported": false
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_linkedin_ads": {
                    "summary": "linkedin_ads detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "linkedin_ads",
                            "name": "LinkedIn Ads",
                            "category": "paid_social",
                            "supports_custom_fields_per_account": false,
                            "data_views": [
                              {
                                "id": "account",
                                "name": "Account"
                              },
                              {
                                "id": "campaign_group",
                                "name": "Campaign group"
                              },
                              {
                                "id": "campaign",
                                "name": "Campaign"
                              },
                              {
                                "id": "creative",
                                "name": "Creative"
                              },
                              {
                                "id": "conversion",
                                "name": "Conversion"
                              }
                            ],
                            "settings": null,
                            "filters": {
                              "supported": true,
                              "combination": "or",
                              "note": "All filter fields are dimensions; type_of_field='dimension' is REQUIRED in each filter (metric filters are not supported). LinkedIn Marketing API only supports OR combination between filters: combination='OR' (default if absent) is accepted; combination='AND' is rejected upfront with a clear error. For multi-value filters (IN/NOT_IN) provide a comma-separated string in `value` (e.g. \"ACTIVE,PAUSED\"). enum fields with `allowed_values` MUST receive one of the listed values; invalid values are rejected upfront.",
                              "fields": [
                                {
                                  "id": "campaignGroupId",
                                  "name": "Campaign group ID",
                                  "category": "id",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ]
                                },
                                {
                                  "id": "campaignId",
                                  "name": "Campaign ID",
                                  "category": "id",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ]
                                },
                                {
                                  "id": "creativeId",
                                  "name": "Creative ID",
                                  "category": "id",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ]
                                },
                                {
                                  "id": "campaignGroupName",
                                  "name": "Campaign group name",
                                  "category": "name",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ]
                                },
                                {
                                  "id": "campaignName",
                                  "name": "Campaign name",
                                  "category": "name",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ]
                                },
                                {
                                  "id": "creativeName",
                                  "name": "Creative name",
                                  "category": "name",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ]
                                },
                                {
                                  "id": "campaignGroupStatus",
                                  "name": "Campaign group status",
                                  "category": "enum",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "ACTIVE",
                                    "ARCHIVED",
                                    "CANCELED",
                                    "COMPLETED",
                                    "DRAFT",
                                    "PAUSED"
                                  ]
                                },
                                {
                                  "id": "campaignStatus",
                                  "name": "Campaign status",
                                  "category": "enum",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "ACTIVE",
                                    "ARCHIVED",
                                    "CANCELED",
                                    "COMPLETED",
                                    "DRAFT",
                                    "PAUSED"
                                  ]
                                },
                                {
                                  "id": "campaignType",
                                  "name": "Campaign type",
                                  "category": "enum",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "TEXT_AD",
                                    "SPONSORED_UPDATES",
                                    "SPONSORED_INMAILS",
                                    "DYNAMIC"
                                  ]
                                },
                                {
                                  "id": "creativeStatus",
                                  "name": "Creative status",
                                  "category": "enum",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "ACTIVE",
                                    "ARCHIVED",
                                    "CANCELED",
                                    "PAUSED",
                                    "DRAFT"
                                  ]
                                },
                                {
                                  "id": "creativeType",
                                  "name": "Creative type",
                                  "category": "enum",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "TEXT_AD",
                                    "IMAGE_AD",
                                    "VIDEO_AD",
                                    "ARTICLE_AD",
                                    "LIVE_VIDEO",
                                    "CAROUSEL_AD",
                                    "DOCUMENT_AD",
                                    "JOBS_AD",
                                    "SPOTLIGHT_AD",
                                    "FOLLOWER_AD",
                                    "EVENT_AD",
                                    "MESSAGE_AD",
                                    "CONVERSATION_AD"
                                  ]
                                }
                              ]
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_linkedin_pages": {
                    "summary": "linkedin_pages detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "linkedin_pages",
                            "name": "LinkedIn Pages",
                            "category": "organic_social",
                            "supports_custom_fields_per_account": false,
                            "data_views": [
                              {
                                "id": "lifetime_followers",
                                "name": "Lifetime follower statistics breakdown"
                              },
                              {
                                "id": "timebound_follower_gains",
                                "name": "Time-bound follower gains statistics"
                              },
                              {
                                "id": "realtime_followers",
                                "name": "Real-time follower count"
                              },
                              {
                                "id": "timebound_share",
                                "name": "Time-bound aggregated posts statistics"
                              },
                              {
                                "id": "specific_shares",
                                "name": "Time-bound specific posts statistics"
                              },
                              {
                                "id": "lifetime_org_page",
                                "name": "Lifetime organization page statistics breakdown"
                              },
                              {
                                "id": "timebound_org_page",
                                "name": "Time-bound organization page statistics"
                              }
                            ],
                            "settings": null,
                            "filters": {
                              "supported": false
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_tiktok_ads": {
                    "summary": "tiktok_ads detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "tiktok_ads",
                            "name": "TikTok Ads",
                            "category": "paid_social",
                            "supports_custom_fields_per_account": false,
                            "data_views": [
                              {
                                "id": "audience",
                                "name": "Audience"
                              },
                              {
                                "id": "account",
                                "name": "Account"
                              },
                              {
                                "id": "campaign",
                                "name": "Campaign"
                              },
                              {
                                "id": "ad_group",
                                "name": "Ad group"
                              },
                              {
                                "id": "ad",
                                "name": "Ad"
                              },
                              {
                                "id": "gmv_max",
                                "name": "GMV Max"
                              }
                            ],
                            "settings": null,
                            "filters": {
                              "supported": true,
                              "combination": "or",
                              "note": "All filter fields are dimensions; type_of_field='dimension' is REQUIRED in each filter (metric filters are not supported). TikTok Marketing API only supports OR combination between filters: combination='OR' (default if absent) is accepted; combination='AND' is rejected upfront with a clear error. For multi-value filters (IN/NOT_IN) provide a comma-separated string in `value` (e.g. 'ENABLE,DISABLE'). enum fields with `allowed_values` MUST receive one of the listed values; invalid values are rejected upfront. AUDIENCE data_view scopes the fields to those with audience-specific report types; ADVERTISER/CAMPAIGN/ADGROUP/AD data_views scope to the corresponding level. Each filter field lists in `data_views` the data views it applies to; gmv_max accepts only campaign fields (campaign_id, campaign_name, campaign_operation_status).",
                              "fields": [
                                {
                                  "id": "campaign_id",
                                  "name": "Campaign ID",
                                  "category": "id",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "data_views": [
                                    "audience",
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad",
                                    "gmv_max"
                                  ]
                                },
                                {
                                  "id": "adgroup_id",
                                  "name": "Ad group ID",
                                  "category": "id",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "data_views": [
                                    "audience",
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ]
                                },
                                {
                                  "id": "ad_id",
                                  "name": "Ad ID",
                                  "category": "id",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "data_views": [
                                    "audience",
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ]
                                },
                                {
                                  "id": "campaign_name",
                                  "name": "Campaign name",
                                  "category": "name",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ],
                                  "data_views": [
                                    "audience",
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad",
                                    "gmv_max"
                                  ]
                                },
                                {
                                  "id": "adgroup_name",
                                  "name": "Ad group name",
                                  "category": "name",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ],
                                  "data_views": [
                                    "audience",
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ]
                                },
                                {
                                  "id": "ad_name",
                                  "name": "Ad name",
                                  "category": "name",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ],
                                  "data_views": [
                                    "audience",
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ]
                                },
                                {
                                  "id": "ad_text",
                                  "name": "Ad text",
                                  "category": "name",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ],
                                  "data_views": [
                                    "audience",
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ]
                                },
                                {
                                  "id": "campaign_type",
                                  "name": "Campaign type",
                                  "category": "enum",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "REGULAR_CAMPAIGN",
                                    "IOS14_CAMPAIGN"
                                  ],
                                  "data_views": [
                                    "audience",
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ]
                                },
                                {
                                  "id": "campaign_budget_mode",
                                  "name": "Campaign budget mode",
                                  "category": "enum",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "BUDGET_MODE_DAY",
                                    "BUDGET_MODE_TOTAL",
                                    "BUDGET_MODE_INFINITE"
                                  ],
                                  "data_views": [
                                    "audience",
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ]
                                },
                                {
                                  "id": "campaign_operation_status",
                                  "name": "Campaign status",
                                  "category": "enum",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "DISABLE",
                                    "ENABLE"
                                  ],
                                  "data_views": [
                                    "audience",
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad",
                                    "gmv_max"
                                  ]
                                },
                                {
                                  "id": "objective_type",
                                  "name": "Objective type",
                                  "category": "enum",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "APP_INSTALL",
                                    "CONVERSIONS",
                                    "REACH",
                                    "TRAFFIC",
                                    "VIDEO_VIEWS",
                                    "SHOP_PURCHASES"
                                  ],
                                  "data_views": [
                                    "audience",
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ]
                                },
                                {
                                  "id": "adgroup_budget_mode",
                                  "name": "Ad group budget mode",
                                  "category": "enum",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "BUDGET_MODE_DAY",
                                    "BUDGET_MODE_TOTAL",
                                    "BUDGET_MODE_INFINITE"
                                  ],
                                  "data_views": [
                                    "audience",
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ]
                                },
                                {
                                  "id": "adgroup_operation_status",
                                  "name": "Ad group status",
                                  "category": "enum",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "DISABLE",
                                    "ENABLE",
                                    "FROZEN"
                                  ],
                                  "data_views": [
                                    "audience",
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ]
                                },
                                {
                                  "id": "optimization_goal",
                                  "name": "Optimization goal",
                                  "category": "enum",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "CLICK",
                                    "CONVERT",
                                    "INSTALL",
                                    "IN_APP_EVENT",
                                    "SHOW",
                                    "REACH",
                                    "LEAD_GENERATION",
                                    "FOLLOWERS",
                                    "PROFILE_VIEWS",
                                    "VALUE",
                                    "MT_LIVE_ROOM",
                                    "PRODUCT_CLICK_IN_LIVE",
                                    "ENGAGED_VIEW",
                                    "ENGAGED_VIEW_FIFTEEN"
                                  ],
                                  "data_views": [
                                    "audience",
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ]
                                },
                                {
                                  "id": "bid_type",
                                  "name": "Bid type",
                                  "category": "enum",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "BID_TYPE_CUSTOM",
                                    "BID_TYPE_NO_BID"
                                  ],
                                  "data_views": [
                                    "audience",
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ]
                                },
                                {
                                  "id": "ad_operation_status",
                                  "name": "Ad status",
                                  "category": "enum",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "DISABLE",
                                    "ENABLE",
                                    "FROZEN"
                                  ],
                                  "data_views": [
                                    "audience",
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ]
                                },
                                {
                                  "id": "ad_format",
                                  "name": "Ad format",
                                  "category": "enum",
                                  "type_of_field": "dimension",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "SINGLE_IMAGE",
                                    "SINGLE_VIDEO",
                                    "CAROUSEL",
                                    "LIVE_CONTENT"
                                  ],
                                  "data_views": [
                                    "audience",
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ]
                                }
                              ]
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_tiktok_organic": {
                    "summary": "tiktok_organic detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "tiktok_organic",
                            "name": "TikTok Organic",
                            "category": "organic_social",
                            "supports_custom_fields_per_account": false,
                            "data_views": [
                              {
                                "id": "profile",
                                "name": "Profile"
                              },
                              {
                                "id": "audience",
                                "name": "Audience"
                              },
                              {
                                "id": "video",
                                "name": "Video"
                              }
                            ],
                            "settings": null,
                            "filters": {
                              "supported": false
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_pinterest_ads": {
                    "summary": "pinterest_ads detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "pinterest_ads",
                            "name": "Pinterest Ads",
                            "category": "paid_social",
                            "supports_custom_fields_per_account": false,
                            "data_views": [
                              {
                                "id": "account",
                                "name": "Account"
                              },
                              {
                                "id": "campaign",
                                "name": "Campaign"
                              },
                              {
                                "id": "ad_group",
                                "name": "Ad group"
                              },
                              {
                                "id": "ad",
                                "name": "Ad"
                              }
                            ],
                            "settings": [
                              {
                                "id": "click_window",
                                "name": "Click attribution window",
                                "description": "Days after a click counted for conversions.",
                                "recommended_value": "30",
                                "options": [
                                  {
                                    "value": "1",
                                    "label": "1 day"
                                  },
                                  {
                                    "value": "7",
                                    "label": "7 days"
                                  },
                                  {
                                    "value": "30",
                                    "label": "30 days"
                                  },
                                  {
                                    "value": "60",
                                    "label": "60 days"
                                  }
                                ]
                              },
                              {
                                "id": "engagement_window",
                                "name": "Engagement attribution window",
                                "description": "Days after an engagement counted for conversions.",
                                "recommended_value": "30",
                                "options": [
                                  {
                                    "value": "1",
                                    "label": "1 day"
                                  },
                                  {
                                    "value": "7",
                                    "label": "7 days"
                                  },
                                  {
                                    "value": "30",
                                    "label": "30 days"
                                  },
                                  {
                                    "value": "60",
                                    "label": "60 days"
                                  }
                                ]
                              },
                              {
                                "id": "view_window",
                                "name": "View attribution window",
                                "description": "Days after a view counted for conversions.",
                                "recommended_value": "1",
                                "options": [
                                  {
                                    "value": "1",
                                    "label": "1 day"
                                  },
                                  {
                                    "value": "7",
                                    "label": "7 days"
                                  },
                                  {
                                    "value": "30",
                                    "label": "30 days"
                                  },
                                  {
                                    "value": "60",
                                    "label": "60 days"
                                  }
                                ]
                              },
                              {
                                "id": "conversion_report_time",
                                "name": "Conversion report time",
                                "description": "Whether conversions are reported by the date of the ad event or the date of the conversion event.",
                                "recommended_value": "TIME_OF_AD_ACTION",
                                "options": [
                                  {
                                    "value": "TIME_OF_AD_ACTION",
                                    "label": "Date of ad event"
                                  },
                                  {
                                    "value": "TIME_OF_CONVERSION",
                                    "label": "Date of conversion event"
                                  }
                                ]
                              }
                            ],
                            "filters": {
                              "supported": true,
                              "combination": "or",
                              "note": "All filter fields are dimensions; type_of_field='dimension' is REQUIRED in each filter (metric filters are not supported). Pinterest Marketing API v5 only applies OR combination between filter conditions: combination='OR' (default if absent) is accepted; combination='AND' is rejected upfront with a clear error. For multi-value filters (IN/NOT_IN) provide a comma-separated string in `value` (e.g. 'ACTIVE,PAUSED'). enum fields with `allowed_values` MUST receive one of the listed values; invalid values are rejected upfront. Filter applicability depends on the resolved data_view: CAMPAIGN_* filters apply when data_view=campaign|ad_group|ad; AD_GROUP_* filters apply when data_view=ad_group|ad; AD_* and CREATIVE_TYPE filters apply when data_view=ad. The API returns an explicit error if an inapplicable filter is submitted.",
                              "fields": [
                                {
                                  "id": "CAMPAIGN_ID",
                                  "name": "Campaign ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "AD_GROUP_ID",
                                  "name": "Ad group ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "AD_ID",
                                  "name": "Ad ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "CAMPAIGN_NAME",
                                  "name": "Campaign name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "AD_GROUP_NAME",
                                  "name": "Ad group name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "AD_NAME",
                                  "name": "Ad name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "CAMPAIGN_STATUS",
                                  "name": "Campaign status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "ACTIVE",
                                    "PAUSED",
                                    "ARCHIVED"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "OBJECTIVE_TYPE",
                                  "name": "Objective type",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "AWARENESS",
                                    "CONSIDERATION",
                                    "VIDEO_VIEW",
                                    "WEB_CONVERSION",
                                    "CATALOG_SALES",
                                    "WEB_SESSIONS",
                                    "AWARENESS_RESERVED",
                                    "ENGAGEMENT"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "AD_GROUP_STATUS",
                                  "name": "Ad group status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "ACTIVE",
                                    "PAUSED",
                                    "ARCHIVED"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "AD_GROUP_SUMMARY_STATUS",
                                  "name": "Ad group summary status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "RUNNING",
                                    "PAUSED",
                                    "NOT_STARTED",
                                    "COMPLETED",
                                    "ADVERTISER_DISABLED",
                                    "ARCHIVED"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "AD_STATUS",
                                  "name": "Ad status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "ACTIVE",
                                    "PAUSED",
                                    "ARCHIVED"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "AD_SUMMARY_STATUS",
                                  "name": "Ad summary status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "APPROVED",
                                    "PAUSED",
                                    "PENDING",
                                    "REJECTED",
                                    "ADVERTISER_DISABLED",
                                    "ARCHIVED"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "AD_REVIEW_STATUS",
                                  "name": "Ad review status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "OTHER",
                                    "PENDING",
                                    "REJECTED",
                                    "APPROVED"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "CREATIVE_TYPE",
                                  "name": "Creative type",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "REGULAR",
                                    "VIDEO",
                                    "SHOPPING",
                                    "CAROUSEL",
                                    "MAX_VIDEO",
                                    "SHOP_THE_PIN",
                                    "STORY"
                                  ],
                                  "type_of_field": "dimension"
                                }
                              ]
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_pinterest_organic": {
                    "summary": "pinterest_organic detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "pinterest_organic",
                            "name": "Pinterest Organic",
                            "category": "organic_social",
                            "supports_custom_fields_per_account": false,
                            "data_views": [
                              {
                                "id": "profile_lifetime",
                                "name": "Profile (lifetime)"
                              },
                              {
                                "id": "profile_timebound",
                                "name": "Profile (time-bound)"
                              },
                              {
                                "id": "top_pins",
                                "name": "Top pins"
                              },
                              {
                                "id": "top_video_pins",
                                "name": "Top video pins"
                              }
                            ],
                            "settings": [
                              {
                                "id": "pinterest_source",
                                "name": "Source",
                                "description": "Whether the pin is owned by the user or by other users.",
                                "applies_to_data_views": [
                                  "profile_timebound",
                                  "top_pins",
                                  "top_video_pins"
                                ],
                                "recommended_value": "ALL",
                                "options": [
                                  {
                                    "value": "ALL",
                                    "label": "All"
                                  },
                                  {
                                    "value": "YOUR_PINS",
                                    "label": "Your pins"
                                  },
                                  {
                                    "value": "OTHER_PINS",
                                    "label": "Other pins"
                                  }
                                ]
                              },
                              {
                                "id": "pinterest_claimed_content",
                                "name": "Claimed content",
                                "description": "Whether the pin links to a website claimed by the user, by other users, or both.",
                                "applies_to_data_views": [
                                  "profile_timebound",
                                  "top_pins",
                                  "top_video_pins"
                                ],
                                "recommended_value": "BOTH",
                                "options": [
                                  {
                                    "value": "BOTH",
                                    "label": "Both"
                                  },
                                  {
                                    "value": "CLAIMED",
                                    "label": "Claimed"
                                  },
                                  {
                                    "value": "OTHER",
                                    "label": "Other"
                                  }
                                ]
                              },
                              {
                                "id": "pinterest_sort_by",
                                "name": "Top pins by",
                                "description": "Metric to sort by. The allowed values depend on the data_view (top_pins exposes 5 values; top_video_pins exposes 9 video-specific values).",
                                "applies_to_data_views": [
                                  "top_pins",
                                  "top_video_pins"
                                ],
                                "recommended_value": "IMPRESSION",
                                "options_by_data_view": {
                                  "top_pins": [
                                    {
                                      "value": "ENGAGEMENT",
                                      "label": "Engagements"
                                    },
                                    {
                                      "value": "IMPRESSION",
                                      "label": "Impressions"
                                    },
                                    {
                                      "value": "OUTBOUND_CLICK",
                                      "label": "Outbound clicks"
                                    },
                                    {
                                      "value": "PIN_CLICK",
                                      "label": "Pin clicks"
                                    },
                                    {
                                      "value": "SAVE",
                                      "label": "Saves"
                                    }
                                  ],
                                  "top_video_pins": [
                                    {
                                      "value": "IMPRESSION",
                                      "label": "Impressions"
                                    },
                                    {
                                      "value": "SAVE",
                                      "label": "Saves"
                                    },
                                    {
                                      "value": "OUTBOUND_CLICK",
                                      "label": "Outbound clicks"
                                    },
                                    {
                                      "value": "VIDEO_MRC_VIEW",
                                      "label": "Video MRC views"
                                    },
                                    {
                                      "value": "VIDEO_AVG_WATCH_TIME",
                                      "label": "Video average watch time"
                                    },
                                    {
                                      "value": "VIDEO_V50_WATCH_TIME",
                                      "label": "Video V50 watch time"
                                    },
                                    {
                                      "value": "QUARTILE_95_PERCENT_VIEW",
                                      "label": "Quartile 95% view"
                                    },
                                    {
                                      "value": "VIDEO_10S_VIEW",
                                      "label": "Video 10s view"
                                    },
                                    {
                                      "value": "VIDEO_START",
                                      "label": "Video start"
                                    }
                                  ]
                                }
                              }
                            ],
                            "filters": {
                              "supported": false
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_snapchat_ads": {
                    "summary": "snapchat_ads detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "snapchat_ads",
                            "name": "Snapchat Ads",
                            "category": "paid_social",
                            "supports_custom_fields_per_account": false,
                            "data_views": [
                              {
                                "id": "account",
                                "name": "Account"
                              },
                              {
                                "id": "demographics_lifetime",
                                "name": "Demographics (lifetime)"
                              },
                              {
                                "id": "campaign",
                                "name": "Campaign"
                              },
                              {
                                "id": "ad_group",
                                "name": "Ad group"
                              },
                              {
                                "id": "ad",
                                "name": "Ad"
                              }
                            ],
                            "settings": [
                              {
                                "id": "click_window",
                                "name": "Click window",
                                "description": "Click attribution window in days.",
                                "recommended_value": "28_DAY",
                                "options": [
                                  {
                                    "value": "1_DAY",
                                    "label": "1 day"
                                  },
                                  {
                                    "value": "7_DAY",
                                    "label": "7 days"
                                  },
                                  {
                                    "value": "28_DAY",
                                    "label": "28 days"
                                  }
                                ]
                              },
                              {
                                "id": "view_window",
                                "name": "View window",
                                "description": "View attribution window. NOTE: value 'none' is lowercase by design; all other values use uppercase.",
                                "recommended_value": "1_DAY",
                                "options": [
                                  {
                                    "value": "none",
                                    "label": "None"
                                  },
                                  {
                                    "value": "1_HOUR",
                                    "label": "1 hour"
                                  },
                                  {
                                    "value": "3_HOUR",
                                    "label": "3 hours"
                                  },
                                  {
                                    "value": "6_HOUR",
                                    "label": "6 hours"
                                  },
                                  {
                                    "value": "1_DAY",
                                    "label": "1 day"
                                  },
                                  {
                                    "value": "7_DAY",
                                    "label": "7 days"
                                  }
                                ]
                              },
                              {
                                "id": "conversion_report_time",
                                "name": "Show conversion metrics by",
                                "description": "Whether conversions are reported by impression time or conversion time.",
                                "recommended_value": "TIME_OF_IMPRESSION",
                                "options": [
                                  {
                                    "value": "TIME_OF_IMPRESSION",
                                    "label": "Impression time"
                                  },
                                  {
                                    "value": "TIME_OF_CONVERSION",
                                    "label": "Conversion time"
                                  }
                                ]
                              }
                            ],
                            "filters": {
                              "supported": true,
                              "combination": "or",
                              "note": "All filter fields are dimensions; type_of_field='dimension' is REQUIRED in each filter (metric filters are not supported). Snapchat Marketing API only supports OR combination between filter conditions: combination='OR' (default if absent) is accepted; combination='AND' is rejected upfront. For multi-value filters (IN/NOT_IN) provide a comma-separated string in `value` (e.g. 'ACTIVE,PAUSED'). enum fields with `allowed_values` MUST receive one of the listed values; invalid values are rejected upfront. Filter applicability is resolved per inferred level: campaign filters apply to campaign|ad_group|ad; ad_group filters apply to ad_group|ad; ad filters apply to ad. The API returns an explicit error if an inapplicable filter is submitted.",
                              "fields": [
                                {
                                  "id": "campaign_id",
                                  "name": "Campaign ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_squad_id",
                                  "name": "Ad group ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_id",
                                  "name": "Ad ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "campaign_name",
                                  "name": "Campaign name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_squad_name",
                                  "name": "Ad group name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_name",
                                  "name": "Ad name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "campaign_status",
                                  "name": "Campaign status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "ACTIVE",
                                    "PAUSED"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "campaign_buy_model",
                                  "name": "Campaign buy model",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "AUCTION",
                                    "RESERVED"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "campaign_objective",
                                  "name": "Campaign objective",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "BRAND_AWARENESS",
                                    "APP_CONVERSION",
                                    "APP_INSTALL",
                                    "CATALOG_SALES",
                                    "ENGAGEMENT",
                                    "LEAD_GENERATION",
                                    "VIDEO_VIEW",
                                    "WEB_CONVERSION",
                                    "PROMOTE_STORIES",
                                    "PROMOTE_PLACES"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_squad_status",
                                  "name": "Ad group status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "ACTIVE",
                                    "PAUSED"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_squad_type",
                                  "name": "Ad group type",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "SNAP_ADS",
                                    "LENS",
                                    "FILTER"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_squad_billing_event",
                                  "name": "Ad group billing event",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "IMPRESSION"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_squad_bid_strategy",
                                  "name": "Ad group bid strategy",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "AUTO_BID",
                                    "LOWEST_COST_WITH_MAX_BID",
                                    "MIN_ROAS",
                                    "TARGET_COST"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_squad_optimization_goal",
                                  "name": "Ad group optimization goal",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "IMPRESSIONS",
                                    "SWIPES",
                                    "APP_INSTALLS",
                                    "VIDEO_VIEWS",
                                    "VIDEO_VIEWS_15_SEC",
                                    "USES",
                                    "STORY_OPENS",
                                    "PIXEL_PAGE_VIEW",
                                    "PIXEL_ADD_TO_CART",
                                    "LANDING_PAGE_VIEW",
                                    "PIXEL_PURCHASE",
                                    "PIXEL_SIGNUP",
                                    "APP_ADD_TO_CART",
                                    "APP_PURCHASE",
                                    "APP_SIGNUP"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_squad_delivery_constraint",
                                  "name": "Ad group delivery constraint",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "DAILY_BUDGET",
                                    "LIFETIME_BUDGET",
                                    "REACH_AND_FREQUENCY"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_squad_pacing_type",
                                  "name": "Ad group pacing type",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "STANDARD",
                                    "ACCELERATED"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_status",
                                  "name": "Ad status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "ACTIVE",
                                    "PAUSED"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_review_status",
                                  "name": "Ad review status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "PENDING",
                                    "APPROVED",
                                    "REJECTED"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_type",
                                  "name": "Ad type",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "SNAP_AD",
                                    "APP_INSTALL",
                                    "REMOTE_WEBPAGE",
                                    "DEEP_LINK",
                                    "STORY",
                                    "AD_TO_LENS",
                                    "AD_TO_CALL",
                                    "AD_TO_MESSAGE",
                                    "FILTER",
                                    "LENS",
                                    "LENS_WEB_VIEW",
                                    "LENS_APP_INSTALL",
                                    "LENS_DEEP_LINK",
                                    "COLLECTION",
                                    "LEAD_GENERATION"
                                  ],
                                  "type_of_field": "dimension"
                                }
                              ]
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_twitter_ads": {
                    "summary": "twitter_ads detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "twitter_ads",
                            "name": "Twitter Ads",
                            "category": "paid_social",
                            "supports_custom_fields_per_account": false,
                            "notes": [
                              "The native Twitter Ads API is asynchronous (the platform queues the query, polls until ready, returns results). For small date ranges this is typically transparent, but large date ranges may exceed the request timeout -- prefer date_range <=28 days.",
                              "Use /v1/query/async when querying twitter_ads with large date ranges (>14 days). Sync /v1/query may approach or exceed the 120s server-side timeout for this integration; async (with /v1/query/status polling) provides predictable behavior."
                            ],
                            "data_views": [
                              {
                                "id": "account",
                                "name": "Account"
                              },
                              {
                                "id": "funding_source",
                                "name": "Funding source"
                              },
                              {
                                "id": "campaign",
                                "name": "Campaign"
                              },
                              {
                                "id": "ad_group",
                                "name": "Ad group"
                              },
                              {
                                "id": "promoted_tweet",
                                "name": "Promoted tweet"
                              }
                            ],
                            "settings": [
                              {
                                "id": "placement",
                                "name": "Placement",
                                "description": "Which inventory to query. ALL_ON_TWITTER includes Twitter app and twitter.com; PUBLISHER_NETWORK is the Twitter Audience Platform (off-Twitter inventory).",
                                "recommended_value": "ALL_ON_TWITTER",
                                "options": [
                                  {
                                    "value": "ALL_ON_TWITTER",
                                    "label": "All on Twitter"
                                  },
                                  {
                                    "value": "PUBLISHER_NETWORK",
                                    "label": "Publisher Network"
                                  }
                                ]
                              }
                            ],
                            "filters": {
                              "supported": true,
                              "combination": "and",
                              "note": "All filter fields are dimensions; type_of_field='dimension' is REQUIRED. Twitter Ads API only supports AND combination between filter conditions: combination='AND' (default if absent) is accepted; combination='OR' is rejected upfront with a clear error. For multi-value filters split conditions across multiple entries in filters[]. Filter applicability is resolved per inferred level: campaignId applies to data_view=campaign|ad_group|promoted_tweet; lineItemId applies to data_view=ad_group|promoted_tweet; promotedTweetId applies to data_view=promoted_tweet. Segmentation types (AGE, GENDER, INTERESTS, KEYWORDS, etc.) appear as OUTPUT dimensions in /v1/fields but are NOT filterable.",
                              "fields": [
                                {
                                  "id": "campaignId",
                                  "name": "Campaign ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "lineItemId",
                                  "name": "Ad group ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "promotedTweetId",
                                  "name": "Promoted tweet ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "fundingInstrumentName",
                                  "name": "Funding source name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "campaignName",
                                  "name": "Campaign name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "lineItemName",
                                  "name": "Ad group name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS"
                                  ],
                                  "type_of_field": "dimension"
                                }
                              ]
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_mailchimp": {
                    "summary": "mailchimp detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "mailchimp",
                            "name": "Mailchimp",
                            "category": "email",
                            "supports_custom_fields_per_account": false,
                            "notes": [
                              "Field ids are exposed without the internal email_ prefix (clients use campaignName, emailsSent, opens, listName, ...). Percentage metrics are returned RAW 0-100 (no /100 scaling). The day and week dimensions are returned as yyyy-MM-dd (for week, the first day of each week, per the week start day set in your Reporting Ninja account) and month as yyyy-MM. The date-time dimensions (sendTime, lastOpenDate, lastClickDate, dateTime, creationDate, lastSubscribeDate, lastUnsubscribeDate) are returned as yyyyMMddHHmmss in the time zone set in your Reporting Ninja account, and are empty when Mailchimp has no value."
                            ],
                            "data_views": [
                              {
                                "id": "campaign_overview",
                                "name": "Campaign overview",
                                "type": "campaign"
                              },
                              {
                                "id": "campaign_24hour_performance",
                                "name": "Campaign - 24-hour performance",
                                "type": "campaign"
                              },
                              {
                                "id": "campaign_clicks",
                                "name": "Campaign - Click details",
                                "type": "campaign"
                              },
                              {
                                "id": "campaign_locations",
                                "name": "Campaign - Locations",
                                "type": "campaign"
                              },
                              {
                                "id": "campaign_domains",
                                "name": "Campaign - Top performing domains",
                                "type": "campaign"
                              },
                              {
                                "id": "list_overview",
                                "name": "List overview",
                                "type": "list"
                              },
                              {
                                "id": "list_activity",
                                "name": "List activity",
                                "type": "list"
                              }
                            ],
                            "settings": [
                              {
                                "id": "entity",
                                "name": "Entity",
                                "description": "REQUIRED. Selects whether /v1/connections lists Mailchimp campaigns or audience lists, and which group of data_views is queryable. campaign_* data_views are valid only when entity=campaign; list_* data_views are valid only when entity=list.",
                                "required": true,
                                "options": [
                                  {
                                    "value": "campaign",
                                    "label": "Campaign"
                                  },
                                  {
                                    "value": "list",
                                    "label": "List"
                                  }
                                ]
                              }
                            ],
                            "filters": {
                              "supported": false
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-18T10:30:00Z"
                      }
                    }
                  },
                  "ok_cross_platform": {
                    "summary": "cross_platform detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "cross_platform",
                            "name": "Cross-platform",
                            "category": "advertising_aggregated",
                            "supports_custom_fields_per_account": true,
                            "notes": [
                              "cross_platform is a meta-integration: each connection is a cross-platform accounts group configured in Reporting Ninja that combines accounts of up to 10 advertising platforms (Google Ads, Microsoft Ads, Facebook Ads, Twitter Ads, LinkedIn Ads, Pinterest Ads, TikTok Ads, Snapchat Ads, Reddit Ads, ChatGPT Ads). Amazon Ads is excluded by the Amazon Ads API terms of service. The metric mapping and the per-platform settings (attribution windows, placements, etc.) belong to the accounts group and are not sent in the request. account_id MUST equal connection_key (both are the key of the accounts group).",
                              "The standard catalog has 19 fields: 10 metrics (impressions, clicks, ctr, avgCPC, cost, conversions, costConv, convRate, totalConvValue, ROAS) and 9 dimensions (account, platform, campaignId, campaignName, campaignStatus, campaignStatusRaw, day, week, month). Field ids are exposed without internal prefixes (e.g. impressions, cost). A query takes at most one time dimension. ctr, avgCPC, costConv, convRate and ROAS are recalculated for each row from their base metrics; ctr, convRate and ROAS are percentages on a 0-100 scale (a return of 3.2 times the cost is a ROAS of 320). data_view is not required.",
                              "campaignStatus takes three stable values that do not depend on the language: active (switched on by the advertiser, whether or not it is delivering), paused and other. campaignStatusRaw is the status as each platform names it. Both are the current status (some networks refresh it every few hours), not the status on each date.",
                              "With campaign dimensions (campaignId, campaignName, campaignStatus, campaignStatusRaw or a campaign name dimension) the rows whose metrics are all zero, requested or not, are left out. campaignName is the current name of the campaign: renaming a campaign moves its history to the new name, and the campaign name dimensions are derived from it. campaignId does not identify a campaign across platforms: request it together with platform. week and month are built from the day, with the week start of the account, and the first week or month is clipped to the start of the date range. Rows are ordered by the text of the requested dimensions, with account first when it is requested (ids do not sort numerically).",
                              "The campaign name dimensions defined in the accounts group are optional fields: /v1/fields lists them after the standard catalog, as derived_<id>, with include_custom_fields=true plus connection_key and account_id. The id of a dimension does not change when it is renamed, and a dimension created with the same name as a dimension of another accounts group of the account takes that id, so a query can be reused with another group. A dimension removed from the accounts group answers FIELD_NOT_AVAILABLE.",
                              "Queries with campaign dimensions read every source at campaign level and may exceed the time of a synchronous request: use /v1/query/async for long date ranges or with the day dimension."
                            ],
                            "data_views": null,
                            "settings": null,
                            "filters": {
                              "supported": false
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-18T10:30:00Z"
                      }
                    }
                  },
                  "ok_hubspot": {
                    "summary": "hubspot detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "hubspot",
                            "name": "HubSpot",
                            "category": "crm",
                            "supports_custom_fields_per_account": true,
                            "notes": [
                              "Field ids are exposed without the internal hs_ prefix. Custom fields for contacts and deals are opt-in via include_custom_fields=true with connection_key + account_id in /v1/fields. /v1/connections returns per-connection metadata blocks (analytics_views[], pipelines_stages[], owners[]) to populate the dynamic setting dropdowns."
                            ],
                            "data_views": [
                              {
                                "id": "analytics_sources",
                                "name": "Sources - Traffic analytics"
                              },
                              {
                                "id": "analytics_countries",
                                "name": "Countries - Traffic analytics"
                              },
                              {
                                "id": "analytics_devices",
                                "name": "Device types - Traffic analytics"
                              },
                              {
                                "id": "analytics_utm_campaigns",
                                "name": "UTM Campaign - Traffic analytics"
                              },
                              {
                                "id": "analytics_utm_sources",
                                "name": "UTM Source - Traffic analytics"
                              },
                              {
                                "id": "analytics_utm_mediums",
                                "name": "UTM Mediums - Traffic analytics"
                              },
                              {
                                "id": "analytics_utm_contents",
                                "name": "UTM Contents - Traffic analytics"
                              },
                              {
                                "id": "analytics_utm_terms",
                                "name": "UTM Terms - Traffic analytics"
                              },
                              {
                                "id": "all_pages",
                                "name": "All page types - Traffic analytics"
                              },
                              {
                                "id": "landing_pages",
                                "name": "Landing pages - Traffic analytics"
                              },
                              {
                                "id": "website_pages",
                                "name": "Website pages - Traffic analytics"
                              },
                              {
                                "id": "blog_posts",
                                "name": "Blog posts - Traffic analytics"
                              },
                              {
                                "id": "knowledge_articles",
                                "name": "Knowledge articles - Traffic analytics"
                              },
                              {
                                "id": "forms",
                                "name": "Forms"
                              },
                              {
                                "id": "marketing_emails",
                                "name": "Emails - Marketing"
                              },
                              {
                                "id": "marketing_campaigns",
                                "name": "Campaigns - Marketing"
                              },
                              {
                                "id": "marketing_device_type",
                                "name": "Device types - Marketing"
                              },
                              {
                                "id": "lifecycle_stages",
                                "name": "Lifecycle stages"
                              },
                              {
                                "id": "contacts",
                                "name": "Contacts"
                              },
                              {
                                "id": "deals",
                                "name": "Deals"
                              }
                            ],
                            "settings": [
                              {
                                "id": "analytics_view",
                                "name": "Analytics view",
                                "applies_to_data_views": [
                                  "analytics_sources",
                                  "analytics_countries",
                                  "analytics_devices",
                                  "analytics_utm_campaigns",
                                  "analytics_utm_sources",
                                  "analytics_utm_mediums",
                                  "analytics_utm_contents",
                                  "analytics_utm_terms",
                                  "all_pages",
                                  "landing_pages",
                                  "website_pages",
                                  "blog_posts",
                                  "knowledge_articles"
                                ],
                                "dynamic": true,
                                "recommended_value": "HUBSPOT_ALL_ANALYTICS_ACTIVITIES"
                              },
                              {
                                "id": "analytics_sources",
                                "name": "Sources",
                                "applies_to_data_views": [
                                  "analytics_sources"
                                ],
                                "multi_select": true,
                                "options": [
                                  {
                                    "value": "direct",
                                    "label": "Direct traffic"
                                  },
                                  {
                                    "value": "organic",
                                    "label": "Organic search"
                                  },
                                  {
                                    "value": "paid",
                                    "label": "Paid search"
                                  },
                                  {
                                    "value": "email",
                                    "label": "Email marketing"
                                  },
                                  {
                                    "value": "referrals",
                                    "label": "Referrals"
                                  },
                                  {
                                    "value": "social",
                                    "label": "Social media"
                                  },
                                  {
                                    "value": "other",
                                    "label": "Other campaigns"
                                  },
                                  {
                                    "value": "offline",
                                    "label": "Offline sources"
                                  }
                                ],
                                "recommended_value": [
                                  "direct",
                                  "organic",
                                  "paid",
                                  "email",
                                  "referrals",
                                  "social",
                                  "other",
                                  "offline"
                                ]
                              },
                              {
                                "id": "queried_date",
                                "name": "Queried date",
                                "applies_to_data_views": [
                                  "contacts"
                                ],
                                "recommended_value": "hs_createdate",
                                "options": [
                                  {
                                    "value": "hs_createdate",
                                    "label": "Create date"
                                  },
                                  {
                                    "value": "hs_hs_lifecyclestage_subscriber_date",
                                    "label": "Became a subscriber date"
                                  },
                                  {
                                    "value": "hs_hs_lifecyclestage_lead_date",
                                    "label": "Became a lead date"
                                  },
                                  {
                                    "value": "hs_hs_lifecyclestage_marketingqualifiedlead_date",
                                    "label": "Became a marketing qualified lead date"
                                  },
                                  {
                                    "value": "hs_hs_lifecyclestage_salesqualifiedlead_date",
                                    "label": "Became a sales qualified lead date"
                                  },
                                  {
                                    "value": "hs_hs_lifecyclestage_opportunity_date",
                                    "label": "Became an opportunity date"
                                  },
                                  {
                                    "value": "hs_hs_lifecyclestage_customer_date",
                                    "label": "Became a customer date"
                                  },
                                  {
                                    "value": "hs_hs_lifecyclestage_evangelist_date",
                                    "label": "Became an evangelist date"
                                  },
                                  {
                                    "value": "hs_hs_lifecyclestage_other_date",
                                    "label": "Became an other lifecycle date"
                                  }
                                ]
                              },
                              {
                                "id": "queried_date",
                                "name": "Queried date",
                                "applies_to_data_views": [
                                  "deals"
                                ],
                                "recommended_value": "hs_createdate",
                                "options": [
                                  {
                                    "value": "hs_createdate",
                                    "label": "Create date"
                                  },
                                  {
                                    "value": "hs_closedate",
                                    "label": "Close date"
                                  },
                                  {
                                    "value": "hs_notes_last_updated",
                                    "label": "Last activity date"
                                  }
                                ]
                              },
                              {
                                "id": "lifecycle_stage",
                                "name": "Lifecycle stage",
                                "applies_to_data_views": [
                                  "contacts"
                                ],
                                "recommended_value": "HUBSPOT_ANY_CONTACT_STAGE",
                                "options": [
                                  {
                                    "value": "HUBSPOT_ANY_CONTACT_STAGE",
                                    "label": "Any"
                                  },
                                  {
                                    "value": "subscriber",
                                    "label": "Subscriber"
                                  },
                                  {
                                    "value": "lead",
                                    "label": "Lead"
                                  },
                                  {
                                    "value": "marketingqualifiedlead",
                                    "label": "Marketing qualified lead"
                                  },
                                  {
                                    "value": "salesqualifiedlead",
                                    "label": "Sales qualified lead"
                                  },
                                  {
                                    "value": "opportunity",
                                    "label": "Opportunity"
                                  },
                                  {
                                    "value": "customer",
                                    "label": "Customer"
                                  },
                                  {
                                    "value": "evangelist",
                                    "label": "Evangelist"
                                  },
                                  {
                                    "value": "other",
                                    "label": "Other lifecycle"
                                  }
                                ]
                              },
                              {
                                "id": "pipeline_stage",
                                "name": "Pipeline and stage",
                                "applies_to_data_views": [
                                  "deals"
                                ],
                                "dynamic": true,
                                "note": "Values returned in /v1/connections (pipelines_stages[] block). Format `pipelineId###stageId`.",
                                "recommended_value": "HUBSPOT_ANY_DEAL_PIPELINE###HUBSPOT_ANY_DEAL_STAGE"
                              },
                              {
                                "id": "owner",
                                "name": "Owner",
                                "applies_to_data_views": [
                                  "contacts",
                                  "deals"
                                ],
                                "dynamic": true,
                                "note": "Values returned in /v1/connections (owners[] block per connection).",
                                "recommended_value": "HUBSPOT_ANY_OWNER"
                              }
                            ],
                            "filters": {
                              "supported": false
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-18T10:30:00Z"
                      }
                    }
                  },
                  "ok_twitter_analytics": {
                    "summary": "twitter_analytics detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "twitter_analytics",
                            "name": "Twitter Analytics",
                            "category": "organic_social",
                            "supports_custom_fields_per_account": false,
                            "notes": [
                              "Twitter Analytics exposes a single profile-lifetime scope (followers, following, tweets, listed counts and derived ratios). Tweets, mentions and search analytics scopes were disabled by the Twitter API v2 migration in April 2023 and are not available through this REST API. fields[] requires at least one metric; the pseudo-dimension twanalytics_account is the only dimension available (optional); no time dimensions, no breakdowns."
                            ],
                            "data_views": null,
                            "settings": null,
                            "filters": {
                              "supported": false
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_reddit_ads": {
                    "summary": "reddit_ads detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "reddit_ads",
                            "name": "Reddit Ads",
                            "category": "paid_social",
                            "supports_custom_fields_per_account": false,
                            "data_views": [
                              {
                                "id": "account",
                                "name": "Account"
                              },
                              {
                                "id": "campaign",
                                "name": "Campaign"
                              },
                              {
                                "id": "ad_group",
                                "name": "Ad group"
                              },
                              {
                                "id": "ad",
                                "name": "Ad"
                              }
                            ],
                            "settings": null,
                            "filters": {
                              "supported": true,
                              "combination": "or",
                              "note": "Reddit Marketing API accepts only POSITIVE operators: EQUALS for id fields, CONTAINS for name fields, IN for enum fields. Negative variants (NOT_EQUALS, NOT_CONTAINS, NOT_IN) are NOT supported and are rejected upfront with a clear error. All filter fields are dimensions; type_of_field='dimension' is REQUIRED in each filter (metric filters are not supported). Combination must be 'OR'; AND is rejected upfront. For multi-value filters (IN) provide a comma-separated string in `value` (e.g. 'ACTIVE,PAUSED'). The 3 enum fields (campaign_effective_status, ad_group_effective_status, ad_effective_status) share the same 11-value list. Filter applicability is resolved per inferred level: campaign filters apply to campaign|ad_group|ad; ad_group filters apply to ad_group|ad; ad filters apply to ad. The API returns an explicit error if an inapplicable filter is submitted.",
                              "fields": [
                                {
                                  "id": "campaign_id",
                                  "name": "Campaign ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_group_id",
                                  "name": "Ad group ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_id",
                                  "name": "Ad ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "campaign_name",
                                  "name": "Campaign name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_group_name",
                                  "name": "Ad group name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_name",
                                  "name": "Ad name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "campaign_effective_status",
                                  "name": "Campaign status",
                                  "category": "enum",
                                  "operators": [
                                    "IN"
                                  ],
                                  "allowed_values": [
                                    "PENDING_BILLING_INFO",
                                    "PENDING_REVIEW",
                                    "PENDING_ID_VERIFICATION",
                                    "ACTIVE",
                                    "REJECTED",
                                    "PAUSED",
                                    "CAMPAIGN_PAUSED",
                                    "AD_GROUP_PAUSED",
                                    "COMPLETED",
                                    "ARCHIVED",
                                    "DELETED"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_group_effective_status",
                                  "name": "Ad group status",
                                  "category": "enum",
                                  "operators": [
                                    "IN"
                                  ],
                                  "allowed_values": [
                                    "PENDING_BILLING_INFO",
                                    "PENDING_REVIEW",
                                    "PENDING_ID_VERIFICATION",
                                    "ACTIVE",
                                    "REJECTED",
                                    "PAUSED",
                                    "CAMPAIGN_PAUSED",
                                    "AD_GROUP_PAUSED",
                                    "COMPLETED",
                                    "ARCHIVED",
                                    "DELETED"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_effective_status",
                                  "name": "Ad status",
                                  "category": "enum",
                                  "operators": [
                                    "IN"
                                  ],
                                  "allowed_values": [
                                    "PENDING_BILLING_INFO",
                                    "PENDING_REVIEW",
                                    "PENDING_ID_VERIFICATION",
                                    "ACTIVE",
                                    "REJECTED",
                                    "PAUSED",
                                    "CAMPAIGN_PAUSED",
                                    "AD_GROUP_PAUSED",
                                    "COMPLETED",
                                    "ARCHIVED",
                                    "DELETED"
                                  ],
                                  "type_of_field": "dimension"
                                }
                              ]
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_klaviyo": {
                    "summary": "klaviyo detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "klaviyo",
                            "name": "Klaviyo",
                            "category": "email",
                            "supports_custom_fields_per_account": false,
                            "notes": [
                              "Metrics of the `percent` field_type (open_rate, click_rate, etc.) are returned in 0-100 scale, not normalized to 0-1. Divide by 100 client-side if a 0-1 representation is needed.",
                              "Conversion metrics (conversions, conversion_rate, conversion_value, etc.) are computed against the account's conversion metric, which is detected automatically."
                            ],
                            "data_views": [
                              {
                                "id": "campaign",
                                "name": "Campaign"
                              },
                              {
                                "id": "flow",
                                "name": "Flow"
                              },
                              {
                                "id": "list",
                                "name": "List"
                              },
                              {
                                "id": "segment",
                                "name": "Segment"
                              },
                              {
                                "id": "form",
                                "name": "Form"
                              }
                            ],
                            "settings": null,
                            "filters": {
                              "supported": true,
                              "combination": "or",
                              "fields": [
                                {
                                  "id": "campaign_id",
                                  "name": "Campaign ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "data_views": [
                                    "campaign"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "campaign_name",
                                  "name": "Campaign name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "campaign"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "campaign_channel",
                                  "name": "Campaign channel",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "email",
                                    "sms",
                                    "mobile_push"
                                  ],
                                  "data_views": [
                                    "campaign"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "campaign_status",
                                  "name": "Campaign status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "Draft",
                                    "Scheduled",
                                    "Preparing to schedule",
                                    "Preparing to send",
                                    "Adding Recipients",
                                    "Sending",
                                    "Sending Segments",
                                    "Sent",
                                    "Variations Sent",
                                    "Queued without Recipients",
                                    "Cancelled",
                                    "Cancelled: Account Disabled",
                                    "Cancelled: Billing Limit",
                                    "Cancelled: Internal Error",
                                    "Cancelled: Misconfigured",
                                    "Cancelled: No Recipients",
                                    "Cancelled: Smart Sending",
                                    "Unknown"
                                  ],
                                  "data_views": [
                                    "campaign"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "flow_id",
                                  "name": "Flow ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "data_views": [
                                    "flow"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "flow_name",
                                  "name": "Flow name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "flow"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "flow_status",
                                  "name": "Flow status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "live",
                                    "manual",
                                    "draft"
                                  ],
                                  "data_views": [
                                    "flow"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "flow_trigger_type",
                                  "name": "Flow trigger type",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "Added to List",
                                    "Date Based",
                                    "Low Inventory",
                                    "Metric",
                                    "Price Drop",
                                    "Unconfigured"
                                  ],
                                  "data_views": [
                                    "flow"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "list_id",
                                  "name": "List ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "data_views": [
                                    "list"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "list_name",
                                  "name": "List name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "list"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "segment_id",
                                  "name": "Segment ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "data_views": [
                                    "segment"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "segment_name",
                                  "name": "Segment name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "segment"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "form_id",
                                  "name": "Form ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "data_views": [
                                    "form"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "form_name",
                                  "name": "Form name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "form"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "form_status",
                                  "name": "Form status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "live",
                                    "draft"
                                  ],
                                  "data_views": [
                                    "form"
                                  ],
                                  "type_of_field": "dimension"
                                }
                              ],
                              "note": "Filter field availability depends on the data_view: each field lists the data views where it is valid in its data_views array, and a filter on a field outside the requested data_view is rejected upfront. All filter fields are dimensions; type_of_field='dimension' is REQUIRED in each filter (metric filters are not supported). Combination must be 'OR'; AND is rejected upfront. Operators per category: EQUALS/NOT_EQUALS for id fields; CONTAINS/NOT_CONTAINS/EQUALS/NOT_EQUALS/BEGINS_WITH for name fields; IN/NOT_IN for enum fields. For multi-value filters (IN/NOT_IN) provide an array of values or a comma-separated string in `value` (e.g. 'email,sms'). Values for the other operators are scalar and must not contain a comma (reserved as the multi-value separator); to match a name that contains a comma, filter by its id with EQUALS or use a comma-free fragment with CONTAINS. Enum values are case-sensitive and must match allowed_values exactly (campaign_status uses display casing such as 'Sent' or 'Preparing to send'; flow_status and form_status are lowercase)."
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_shopify": {
                    "summary": "shopify detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "shopify",
                            "name": "Shopify",
                            "category": "ecommerce",
                            "supports_custom_fields_per_account": false,
                            "notes": [
                              "Metrics of the `percent` field_type (discount_rate, returning_customer_rate) are returned in 0-100 scale, not normalized to 0-1. Divide by 100 client-side if a 0-1 representation is needed.",
                              "All money metrics are returned in the store currency (the `currency` attribute of the connection's account in /v1/connections). The currency_code dimension is the customer-facing currency of each order and does not change the amounts.",
                              "The return metrics (returns, refunded_amount, refunded_orders, units_returned, product_returns) are attributed to the refund date, matching Shopify Analytics; all other metrics are attributed to the order processed date.",
                              "The utm_campaign, utm_source and utm_medium dimensions describe the customer's first visit; the last_interaction_source and last_interaction_source_type dimensions describe the visit that preceded the order.",
                              "Date ranges are limited to 366 days. Orders older than 60 days additionally require the read_all_orders access scope on the connected app: a query whose start date is older than 60 days ago is rejected until that scope is granted."
                            ],
                            "data_views": [
                              {
                                "id": "orders",
                                "name": "Orders"
                              },
                              {
                                "id": "products",
                                "name": "Products"
                              },
                              {
                                "id": "inventory",
                                "name": "Inventory"
                              }
                            ],
                            "settings": null,
                            "filters": {
                              "supported": true,
                              "combination": "and_or",
                              "fields": [
                                {
                                  "id": "order_id",
                                  "name": "Order ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "data_views": [
                                    "orders"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "order_name",
                                  "name": "Order name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "orders"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "sales_channel",
                                  "name": "Sales channel",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "orders"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "referrer_source",
                                  "name": "Referrer source",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "orders"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "referrer_source_type",
                                  "name": "Referrer source type",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "ABANDONED_CART",
                                    "AD",
                                    "AFFILIATE",
                                    "LINK",
                                    "LOYALTY",
                                    "MESSAGE",
                                    "NEWSLETTER",
                                    "NOTIFICATION",
                                    "POST",
                                    "RETARGETING",
                                    "SEO",
                                    "STOREFRONT_APP",
                                    "TRANSACTIONAL"
                                  ],
                                  "data_views": [
                                    "orders"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "utm_campaign",
                                  "name": "UTM campaign",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "orders"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "utm_source",
                                  "name": "UTM source",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "orders"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "utm_medium",
                                  "name": "UTM medium",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "orders"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "shipping_country",
                                  "name": "Shipping country",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "orders"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "shipping_province",
                                  "name": "Shipping province",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "orders"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "currency_code",
                                  "name": "Currency code",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "orders"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "discount_code",
                                  "name": "Discount code",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "orders"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "financial_status",
                                  "name": "Financial status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "AUTHORIZED",
                                    "EXPIRED",
                                    "PAID",
                                    "PARTIALLY_PAID",
                                    "PARTIALLY_REFUNDED",
                                    "PENDING",
                                    "REFUNDED",
                                    "VOIDED"
                                  ],
                                  "data_views": [
                                    "orders"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "fulfillment_status",
                                  "name": "Fulfillment status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "FULFILLED",
                                    "IN_PROGRESS",
                                    "ON_HOLD",
                                    "PARTIALLY_FULFILLED",
                                    "REQUEST_DECLINED",
                                    "SCHEDULED",
                                    "UNFULFILLED"
                                  ],
                                  "data_views": [
                                    "orders"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "order_tags",
                                  "name": "Order tags",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "orders",
                                    "products"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "customer_type",
                                  "name": "Customer type",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "First-time",
                                    "Returning",
                                    "Unknown"
                                  ],
                                  "data_views": [
                                    "orders",
                                    "products"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "product_id",
                                  "name": "Product ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "data_views": [
                                    "products",
                                    "inventory"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "product_title",
                                  "name": "Product title",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "products",
                                    "inventory"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "product_vendor",
                                  "name": "Product vendor",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "products",
                                    "inventory"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "product_type",
                                  "name": "Product type",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "products",
                                    "inventory"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "variant_id",
                                  "name": "Variant ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "data_views": [
                                    "products",
                                    "inventory"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "variant_title",
                                  "name": "Variant title",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "products",
                                    "inventory"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "sku",
                                  "name": "SKU",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "products",
                                    "inventory"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "product_status",
                                  "name": "Product status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "ACTIVE",
                                    "ARCHIVED",
                                    "DRAFT",
                                    "UNLISTED"
                                  ],
                                  "data_views": [
                                    "inventory"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "location",
                                  "name": "Location",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "inventory"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "location_id",
                                  "name": "Location ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "data_views": [
                                    "inventory"
                                  ],
                                  "type_of_field": "dimension"
                                }
                              ],
                              "note": "Filter field availability depends on the data_view: each field lists the data views where it is valid in its data_views array, and a filter on a field outside the requested data_view is rejected upfront. All filter fields are dimensions; type_of_field='dimension' is REQUIRED in each filter (metric filters are not supported). Conditions are combined with a single combination, 'AND' (every condition must match) or 'OR' (any match; the default): all conditions must share the same combination and mixes are rejected upfront. Operators per category: EQUALS/NOT_EQUALS for id fields; CONTAINS/NOT_CONTAINS/EQUALS/NOT_EQUALS/BEGINS_WITH for name fields; IN/NOT_IN for enum fields. For multi-value filters (IN/NOT_IN) provide an array of values or a comma-separated string in `value` (e.g. 'PAID,REFUNDED'). Values for the other operators are scalar and must not contain a comma (reserved as the multi-value separator); to match a name that contains a comma, filter by its id with EQUALS or use a comma-free fragment with CONTAINS. Enum values are case-sensitive and must match allowed_values exactly (referrer_source_type, financial_status, fulfillment_status and product_status use provider casing such as 'PARTIALLY_REFUNDED'; customer_type uses display casing: 'First-time', 'Returning', 'Unknown')."
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_chatgpt_ads": {
                    "summary": "chatgpt_ads detail",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "chatgpt_ads",
                            "name": "ChatGPT Ads",
                            "category": "paid_ai",
                            "supports_custom_fields_per_account": false,
                            "notes": [
                              "Metrics of the `percent` field_type (ctr, conversion_rate) are returned in 0-100 scale, not normalized to 0-1. Divide by 100 client-side if a 0-1 representation is needed.",
                              "All money metrics (spend, cpc, cpm, cost_per_conversion) are returned in the ad account currency (the `currency` attribute of the connection's account in /v1/connections).",
                              "The segment dimensions (country_name; device_type; the product_* dimensions) cannot be combined with each other in one query, filters on them included: request one segment family at a time.",
                              "The conversion metrics (conversions, view_through_conversions, conversion_rate, cost_per_conversion) are period totals: they are not available together with the day, week or month dimensions, nor with the segment dimensions.",
                              "`conversions` counts click-through conversions; `view_through_conversions` counts post-impression conversions with a fixed 1-day window and is informational, as documented by OpenAI.",
                              "Date ranges are limited to 366 days.",
                              "Daily, weekly and monthly buckets follow the time zone of the ad account."
                            ],
                            "data_views": [
                              {
                                "id": "account",
                                "name": "Account"
                              },
                              {
                                "id": "campaign",
                                "name": "Campaign"
                              },
                              {
                                "id": "ad_group",
                                "name": "Ad group"
                              },
                              {
                                "id": "ad",
                                "name": "Ad"
                              }
                            ],
                            "settings": null,
                            "filters": {
                              "supported": true,
                              "combination": "and_or",
                              "fields": [
                                {
                                  "id": "account_id",
                                  "name": "Account ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "data_views": [
                                    "account"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "account_name",
                                  "name": "Account name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "account"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "campaign_id",
                                  "name": "Campaign ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "data_views": [
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "campaign_name",
                                  "name": "Campaign name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "campaign_status",
                                  "name": "Campaign status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "active",
                                    "paused",
                                    "archived"
                                  ],
                                  "data_views": [
                                    "campaign"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_group_id",
                                  "name": "Ad group ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "data_views": [
                                    "ad_group",
                                    "ad"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_group_name",
                                  "name": "Ad group name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "ad_group",
                                    "ad"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_group_status",
                                  "name": "Ad group status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "active",
                                    "paused",
                                    "archived"
                                  ],
                                  "data_views": [
                                    "ad_group"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_id",
                                  "name": "Ad ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "data_views": [
                                    "ad"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_name",
                                  "name": "Ad name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "ad"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_title",
                                  "name": "Ad title",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "ad"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_status",
                                  "name": "Ad status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "active",
                                    "paused",
                                    "archived"
                                  ],
                                  "data_views": [
                                    "ad"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "ad_review_status",
                                  "name": "Ad review status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "in_review",
                                    "processing",
                                    "approved",
                                    "rejected"
                                  ],
                                  "data_views": [
                                    "ad"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "country_name",
                                  "name": "Country",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "product_item_id",
                                  "name": "Product item ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "data_views": [
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "product_title",
                                  "name": "Product title",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "product_brand",
                                  "name": "Product brand",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "device_type",
                                  "name": "Device type",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS",
                                    "BEGINS_WITH"
                                  ],
                                  "data_views": [
                                    "account",
                                    "campaign",
                                    "ad_group",
                                    "ad"
                                  ],
                                  "type_of_field": "dimension"
                                }
                              ],
                              "note": "Filter field availability depends on the data_view: each field lists the data views where it is valid in its data_views array, and a filter on a field outside the requested data_view is rejected upfront. All filter fields are dimensions; type_of_field='dimension' is REQUIRED in each filter (metric filters are not supported). Conditions are combined with a single combination, 'AND' (every condition must match) or 'OR' (any match; the default): all conditions must share the same combination and mixes are rejected upfront. Operators per category: EQUALS/NOT_EQUALS for id fields; CONTAINS/NOT_CONTAINS/EQUALS/NOT_EQUALS/BEGINS_WITH for name fields; IN/NOT_IN for enum fields. For multi-value filters (IN/NOT_IN) provide an array of values or a comma-separated string in `value` (e.g. 'active,paused'). Values for the other operators are scalar and must not contain a comma (reserved as the multi-value separator); to match a name that contains a comma, filter by its id with EQUALS or use a comma-free fragment with CONTAINS. Enum values are case-sensitive and must match allowed_values exactly (campaign_status, ad_group_status, ad_status and ad_review_status use the provider's lowercase values such as 'active' or 'in_review')."
                            },
                            "queryable": true
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-17T10:30:00Z"
                      }
                    }
                  },
                  "ok_amazon_ads": {
                    "summary": "Retail media. Seven data views, three settings, filters joined with OR only, and queryable: false -- its structure is published so widgets and reports can be built with it, and only reading its rows through /v1/query is refused.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "integrations": [
                          {
                            "id": "amazon_ads",
                            "name": "Amazon Ads",
                            "category": "retail_media",
                            "supports_custom_fields_per_account": false,
                            "data_views": [
                              {
                                "id": "campaign",
                                "name": "Campaign",
                                "description": "Campaign-level performance."
                              },
                              {
                                "id": "ad_group",
                                "name": "Ad group",
                                "description": "Ad group-level performance."
                              },
                              {
                                "id": "product_ad",
                                "name": "Product ad",
                                "description": "Performance of each advertised product."
                              },
                              {
                                "id": "target",
                                "name": "Target",
                                "description": "Performance of each targeting expression."
                              },
                              {
                                "id": "keyword",
                                "name": "Keyword",
                                "description": "Performance of each keyword, with its match type."
                              },
                              {
                                "id": "asin",
                                "name": "ASIN",
                                "description": "Performance attributed to each purchased ASIN."
                              },
                              {
                                "id": "ad",
                                "name": "Ad",
                                "description": "Performance of each Sponsored Brands ad."
                              }
                            ],
                            "settings": [
                              {
                                "id": "ad_types",
                                "name": "Ad types",
                                "description": "The ad types the report covers, as a comma-separated list. The fields available are the union of the fields of the selected ad types, and each data view accepts only the ad types Amazon reports it for, listed in options_by_data_view. Defaults to the first ad type of the data view.",
                                "required": false,
                                "multi_select": true,
                                "options": [
                                  {
                                    "value": "sponsored_products",
                                    "label": "Sponsored Products"
                                  },
                                  {
                                    "value": "sponsored_brands",
                                    "label": "Sponsored Brands"
                                  },
                                  {
                                    "value": "sponsored_display",
                                    "label": "Sponsored Display"
                                  }
                                ],
                                "options_by_data_view": {
                                  "campaign": [
                                    {
                                      "value": "sponsored_products",
                                      "label": "Sponsored Products"
                                    },
                                    {
                                      "value": "sponsored_brands",
                                      "label": "Sponsored Brands"
                                    },
                                    {
                                      "value": "sponsored_display",
                                      "label": "Sponsored Display"
                                    }
                                  ],
                                  "ad_group": [
                                    {
                                      "value": "sponsored_products",
                                      "label": "Sponsored Products"
                                    },
                                    {
                                      "value": "sponsored_brands",
                                      "label": "Sponsored Brands"
                                    },
                                    {
                                      "value": "sponsored_display",
                                      "label": "Sponsored Display"
                                    }
                                  ],
                                  "product_ad": [
                                    {
                                      "value": "sponsored_products",
                                      "label": "Sponsored Products"
                                    },
                                    {
                                      "value": "sponsored_display",
                                      "label": "Sponsored Display"
                                    }
                                  ],
                                  "target": [
                                    {
                                      "value": "sponsored_products",
                                      "label": "Sponsored Products"
                                    },
                                    {
                                      "value": "sponsored_brands",
                                      "label": "Sponsored Brands"
                                    },
                                    {
                                      "value": "sponsored_display",
                                      "label": "Sponsored Display"
                                    }
                                  ],
                                  "keyword": [
                                    {
                                      "value": "sponsored_products",
                                      "label": "Sponsored Products"
                                    },
                                    {
                                      "value": "sponsored_brands",
                                      "label": "Sponsored Brands"
                                    }
                                  ],
                                  "asin": [
                                    {
                                      "value": "sponsored_products",
                                      "label": "Sponsored Products"
                                    },
                                    {
                                      "value": "sponsored_display",
                                      "label": "Sponsored Display"
                                    }
                                  ],
                                  "ad": [
                                    {
                                      "value": "sponsored_brands",
                                      "label": "Sponsored Brands"
                                    }
                                  ]
                                }
                              },
                              {
                                "id": "creative_type",
                                "name": "Creative type",
                                "description": "Restricts a Sponsored Brands report to video or to non-video creatives. Valid only when ad_types includes sponsored_brands; defaults to all. The ad data view is reported for the creative type all only, so it accepts no other value.",
                                "recommended_value": "all",
                                "options": [
                                  {
                                    "value": "all",
                                    "label": "All creatives"
                                  },
                                  {
                                    "value": "video",
                                    "label": "Video creatives"
                                  },
                                  {
                                    "value": "non_video",
                                    "label": "Non-video creatives"
                                  }
                                ]
                              },
                              {
                                "id": "tactics",
                                "name": "Tactics",
                                "description": "The Sponsored Display tactics the report covers, as a comma-separated list. Valid only when ad_types includes sponsored_display; defaults to product_targeting.",
                                "multi_select": true,
                                "recommended_value": "product_targeting",
                                "options": [
                                  {
                                    "value": "product_targeting",
                                    "label": "Product targeting"
                                  },
                                  {
                                    "value": "audience_targeting",
                                    "label": "Audience targeting"
                                  }
                                ]
                              }
                            ],
                            "filters": {
                              "supported": true,
                              "combination": "or",
                              "fields": [
                                {
                                  "id": "adId",
                                  "name": "Ad ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "adGroupId",
                                  "name": "Ad group ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "adGroupName",
                                  "name": "Ad group name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "asin",
                                  "name": "Advertised ASIN",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "campaignId",
                                  "name": "Campaign ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "campaignName",
                                  "name": "Campaign name",
                                  "category": "name",
                                  "operators": [
                                    "CONTAINS",
                                    "NOT_CONTAINS",
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "campaignStatus",
                                  "name": "Campaign status",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "enabled",
                                    "paused",
                                    "archived"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "keywordId",
                                  "name": "Keyword ID",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "matchType",
                                  "name": "Match type",
                                  "category": "enum",
                                  "operators": [
                                    "IN",
                                    "NOT_IN"
                                  ],
                                  "allowed_values": [
                                    "BROAD",
                                    "PHRASE",
                                    "EXACT"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "otherAsin",
                                  "name": "Other ASIN",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                },
                                {
                                  "id": "sku",
                                  "name": "SKU",
                                  "category": "id",
                                  "operators": [
                                    "EQUALS",
                                    "NOT_EQUALS"
                                  ],
                                  "type_of_field": "dimension"
                                }
                              ],
                              "note": "Amazon Ads filters are written in the definition of a widget, since Amazon Ads data cannot be queried through the API. Each condition names a field, an operator and a value; type_of_field is set by the platform and is not part of the condition. The conditions of a widget are always combined with OR. The same eleven fields apply to every data view. The IN and NOT_IN operators take several values at once in a single comma-separated value, and a field that lists allowed_values accepts only the values listed there."
                            },
                            "queryable": false,
                            "not_queryable_reason": "Amazon Ads data cannot be queried through the API under the Amazon Ads API terms. Its reports and widgets can be managed through /v1/reports and /v1/widgets, and its fields and connections are listed as for any other integration."
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/connections": {
      "post": {
        "operationId": "listConnections",
        "tags": [
          "Connections"
        ],
        "summary": "List connections for an integration",
        "description": "A connection represents an OAuth-authorized link to the underlying platform (e.g. a Meta user with access to multiple Ad Accounts). Each connection groups several account_ids. An integration the account has no connection for answers an empty list, not an error.",
        "x-mcp-description": "Use this tool to discover which accounts the user can query. Pass the integration_id (from /integrations) and you get back a list of connection_key + accounts, each with account_id and account_name. You'll use the pair (connection_key, account_id) in subsequent calls to /fields and /query.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConnectionsRequest"
              },
              "examples": {
                "facebook_ads": {
                  "summary": "Facebook Ads (no settings required)",
                  "value": {
                    "integration_id": "facebook_ads"
                  }
                },
                "mailchimp_campaign": {
                  "summary": "Mailchimp (entity=campaign; response includes pseudo-account ALL_CAMPAIGNS_SENT_IN_DATE_RANGE)",
                  "value": {
                    "integration_id": "mailchimp",
                    "settings": {
                      "entity": "campaign"
                    }
                  }
                },
                "mailchimp_list": {
                  "summary": "Mailchimp (entity=list)",
                  "value": {
                    "integration_id": "mailchimp",
                    "settings": {
                      "entity": "list"
                    }
                  }
                },
                "cross_platform": {
                  "summary": "Cross-platform (returns the cross-platform accounts groups of the account; each group becomes a connection whose single account has account_id = connection_key)",
                  "value": {
                    "integration_id": "cross_platform"
                  }
                },
                "hubspot": {
                  "summary": "HubSpot (response also includes analytics_views[], pipelines_stages[], owners[] per connection -- used to build the dynamic settings dropdowns)",
                  "value": {
                    "integration_id": "hubspot"
                  }
                },
                "klaviyo": {
                  "summary": "Klaviyo (one account per connection; accounts include currency when the Klaviyo account declares a preferred currency)",
                  "value": {
                    "integration_id": "klaviyo"
                  }
                },
                "shopify": {
                  "summary": "Shopify (one store per connection; connection_key = account_id = the shop id; accounts include the store currency)",
                  "value": {
                    "integration_id": "shopify"
                  }
                },
                "chatgpt_ads": {
                  "summary": "ChatGPT Ads (one ad account per connection; connection_key = account_id = the ad account id; accounts include the ad account currency)",
                  "value": {
                    "integration_id": "chatgpt_ads"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer of the call, or a business error envelope. This endpoint reports its own errors with HTTP 200 and status=error: read envelope.status before reading data.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ConnectionsListResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    }
                  ]
                },
                "examples": {
                  "ok": {
                    "value": {
                      "status": "ok",
                      "data": {
                        "connections": [
                          {
                            "connection_key": "consumer@example.com",
                            "connection_name": "consumer@example.com",
                            "status": "active",
                            "accounts": [
                              {
                                "account_id": "0000000000000000",
                                "account_name": "My Meta Ads account",
                                "currency": "EUR"
                              }
                            ]
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-16T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/fields": {
      "post": {
        "operationId": "listFields",
        "tags": [
          "Fields"
        ],
        "summary": "List dimensions and metrics for a connection",
        "description": "Returns the set of field_ids you can pass to /v1/query, plus default_dimension and default_metric suggestions. By default returns the standard catalog plus the calculated metrics defined in the account for the integration, listed as regular metrics whose field_id can be requested in /v1/query.fields. To include account-specific custom fields (e.g. Facebook Ads custom conversions, GA4 custom metrics and dimensions, or the campaign name dimensions of a cross_platform accounts group), set include_custom_fields=true and provide connection_key + account_id. include_custom_fields is only allowed when the integration's catalog declares supports_custom_fields_per_account=true. For all other integrations, sending include_custom_fields (or connection_key/account_id) returns an error.",
        "x-mcp-description": "Use this tool to discover the available dimensions and metrics for a given account before composing a query. Field IDs returned here are exactly what you should pass in /v1/query.fields (without integration-specific prefixes). The naming convention depends on the integration: facebook_ads uses underscores (e.g. `campaign_name`, `day`, `impressions`, `clicks`, `spend`), google_ads uses dotted paths (e.g. `campaign.name`, `segments.date`, `metrics.impressions`, `metrics.clicks`, `metrics.cost_micros`). The `dim_met` attribute tells you whether a field is a dimension or a metric.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FieldsRequest"
              },
              "examples": {
                "facebook_ads_standard": {
                  "summary": "Facebook Ads -- standard catalog only (no custom conversions)",
                  "value": {
                    "integration_id": "facebook_ads"
                  }
                },
                "facebook_ads_with_custom": {
                  "summary": "Facebook Ads -- include account-specific custom conversions",
                  "value": {
                    "integration_id": "facebook_ads",
                    "include_custom_fields": true,
                    "connection_key": "consumer@example.com",
                    "account_id": "0000000000000000"
                  }
                },
                "google_ads": {
                  "summary": "Google Ads (data_view required; no custom fields supported)",
                  "value": {
                    "integration_id": "google_ads",
                    "data_view": "campaign"
                  }
                },
                "ga4_standard": {
                  "summary": "GA4 -- standard catalog only (no custom dimensions/metrics)",
                  "value": {
                    "integration_id": "ga4"
                  }
                },
                "ga4_with_custom": {
                  "summary": "GA4 -- include account-specific custom dimensions and metrics",
                  "value": {
                    "integration_id": "ga4",
                    "include_custom_fields": true,
                    "connection_key": "user@example.com",
                    "account_id": "properties/123456789"
                  }
                },
                "microsoft_ads": {
                  "summary": "Microsoft Ads (data_view required; no custom fields supported)",
                  "value": {
                    "integration_id": "microsoft_ads",
                    "data_view": "campaign_performance"
                  }
                },
                "youtube": {
                  "summary": "YouTube (data_view required; no custom fields supported)",
                  "value": {
                    "integration_id": "youtube",
                    "data_view": "basic"
                  }
                },
                "google_search_console": {
                  "summary": "Google Search Console (no data_view; no custom fields supported)",
                  "value": {
                    "integration_id": "google_search_console"
                  }
                },
                "google_business_profile": {
                  "summary": "Google Business Profile (data_view required; no custom fields supported)",
                  "value": {
                    "integration_id": "google_business_profile",
                    "data_view": "performance"
                  }
                },
                "facebook_insights": {
                  "summary": "Facebook Insights (data_view required; no custom fields supported)",
                  "value": {
                    "integration_id": "facebook_insights",
                    "data_view": "page"
                  }
                },
                "instagram_insights": {
                  "summary": "Instagram Insights (data_view required; no custom fields supported)",
                  "value": {
                    "integration_id": "instagram_insights",
                    "data_view": "account"
                  }
                },
                "linkedin_ads": {
                  "summary": "LinkedIn Ads (data_view required; no custom fields supported)",
                  "value": {
                    "integration_id": "linkedin_ads",
                    "data_view": "campaign"
                  }
                },
                "linkedin_pages": {
                  "summary": "LinkedIn Pages (data_view required; no custom fields supported)",
                  "value": {
                    "integration_id": "linkedin_pages",
                    "data_view": "lifetime_followers"
                  }
                },
                "tiktok_ads": {
                  "summary": "TikTok Ads (data_view required; no custom fields supported)",
                  "value": {
                    "integration_id": "tiktok_ads",
                    "data_view": "campaign"
                  }
                },
                "tiktok_organic": {
                  "summary": "TikTok Organic (data_view required; no custom fields supported)",
                  "value": {
                    "integration_id": "tiktok_organic",
                    "data_view": "profile"
                  }
                },
                "pinterest_ads": {
                  "summary": "Pinterest Ads (data_view required; no custom fields supported)",
                  "value": {
                    "integration_id": "pinterest_ads",
                    "data_view": "campaign"
                  }
                },
                "pinterest_organic": {
                  "summary": "Pinterest Organic (data_view required; no custom fields supported)",
                  "value": {
                    "integration_id": "pinterest_organic",
                    "data_view": "top_pins"
                  }
                },
                "snapchat_ads": {
                  "summary": "Snapchat Ads (data_view required; no custom fields supported)",
                  "value": {
                    "integration_id": "snapchat_ads",
                    "data_view": "campaign"
                  }
                },
                "reddit_ads": {
                  "summary": "Reddit Ads (data_view required; no custom fields supported)",
                  "value": {
                    "integration_id": "reddit_ads",
                    "data_view": "campaign"
                  }
                },
                "twitter_analytics": {
                  "summary": "Twitter Analytics (data_view NOT required -- single hardcoded scope; no custom fields supported)",
                  "value": {
                    "integration_id": "twitter_analytics"
                  }
                },
                "twitter_ads": {
                  "summary": "Twitter Ads (data_view required; no custom fields supported)",
                  "value": {
                    "integration_id": "twitter_ads",
                    "data_view": "campaign"
                  }
                },
                "mailchimp_campaign_overview": {
                  "summary": "Mailchimp (entity=campaign + data_view=campaign_overview; field ids returned WITHOUT internal email_ prefix)",
                  "value": {
                    "integration_id": "mailchimp",
                    "settings": {
                      "entity": "campaign"
                    },
                    "data_view": "campaign_overview"
                  }
                },
                "mailchimp_list_activity": {
                  "summary": "Mailchimp (entity=list + data_view=list_activity)",
                  "value": {
                    "integration_id": "mailchimp",
                    "settings": {
                      "entity": "list"
                    },
                    "data_view": "list_activity"
                  }
                },
                "cross_platform": {
                  "summary": "Cross-platform (no data_view; field ids returned WITHOUT internal cx: prefix; standard catalog of 19 fields; campaign name dimensions with include_custom_fields)",
                  "value": {
                    "integration_id": "cross_platform"
                  }
                },
                "cross_platform_with_custom": {
                  "summary": "Cross-platform -- include the campaign name dimensions of the accounts group, listed after the standard catalog as derived_<id> (account_id MUST equal connection_key)",
                  "value": {
                    "integration_id": "cross_platform",
                    "include_custom_fields": true,
                    "connection_key": "<group_uuid>",
                    "account_id": "<group_uuid>"
                  }
                },
                "hubspot_analytics_sources": {
                  "summary": "HubSpot (data_view=analytics_sources; field ids returned WITHOUT internal hs_ prefix)",
                  "value": {
                    "integration_id": "hubspot",
                    "data_view": "analytics_sources"
                  }
                },
                "hubspot_contacts_custom_fields": {
                  "summary": "HubSpot (data_view=contacts with include_custom_fields=true to receive per-account custom contact properties; connection_key + account_id REQUIRED)",
                  "value": {
                    "integration_id": "hubspot",
                    "data_view": "contacts",
                    "include_custom_fields": true,
                    "connection_key": "<hubspot_credential_id>",
                    "account_id": "<hubspot_credential_id>"
                  }
                },
                "klaviyo": {
                  "summary": "Klaviyo (data_view required; no custom fields supported)",
                  "value": {
                    "integration_id": "klaviyo",
                    "data_view": "campaign"
                  }
                },
                "shopify": {
                  "summary": "Shopify (data_view required; no custom fields supported)",
                  "value": {
                    "integration_id": "shopify",
                    "data_view": "orders"
                  }
                },
                "chatgpt_ads": {
                  "summary": "ChatGPT Ads (data_view required: account, campaign, ad_group or ad; no per-account custom fields, so connection_key and account_id are not accepted)",
                  "value": {
                    "integration_id": "chatgpt_ads",
                    "data_view": "campaign"
                  }
                },
                "amazon_ads": {
                  "summary": "amazon_ads takes a data_view and, optionally, settings.ad_types: a comma separated list of ad types offered by that data view. Without it the fields of every ad type of the view come back.",
                  "value": {
                    "integration_id": "amazon_ads",
                    "data_view": "campaign",
                    "settings": {
                      "ad_types": "sponsored_products"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer of the call, or a business error envelope. This endpoint reports its own errors with HTTP 200 and status=error: read envelope.status before reading data.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/FieldsListResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    }
                  ]
                },
                "examples": {
                  "ok": {
                    "value": {
                      "status": "ok",
                      "data": {
                        "fields": [
                          {
                            "field_id": "impressions",
                            "field_name": "Impressions",
                            "field_description": "The number of times your ads were on screen",
                            "field_type": "number",
                            "dim_met": "metric"
                          },
                          {
                            "field_id": "campaign_name",
                            "field_name": "Campaign",
                            "field_description": "",
                            "field_type": "text",
                            "dim_met": "dimension"
                          }
                        ],
                        "default_dimension": "account_name",
                        "default_metric": "impressions"
                      },
                      "meta": {
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-16T10:30:00Z"
                      }
                    }
                  },
                  "ok_amazon_ads": {
                    "summary": "The first fields of the campaign view for sponsored products (abbreviated).",
                    "value": {
                      "status": "ok",
                      "data": {
                        "fields": [
                          {
                            "field_id": "attributedConversions1dSameSKU",
                            "field_name": "1 Day Advertised SKU Orders",
                            "field_description": "Number of attributed conversion events occurring within 24 hours of ad click where the purchased SKU was the same as the SKU advertised.",
                            "field_type": "number",
                            "dim_met": "metric"
                          },
                          {
                            "field_id": "attributedSales1dSameSKU",
                            "field_name": "1 Day Advertised SKU Sales",
                            "field_description": "Total value of sales occurring within 24 hours of ad click where the purchased SKU was the same as the SKU advertised.",
                            "field_type": "currency",
                            "dim_met": "metric"
                          },
                          {
                            "field_id": "attributedUnitsOrdered1dSameSKU",
                            "field_name": "1 Day Advertised SKU Units",
                            "field_description": "Total number of units ordered within 24 hours of ad click where the purchased SKU was the same as the SKU advertised.",
                            "field_type": "number",
                            "dim_met": "metric"
                          },
                          {
                            "field_id": "attributedConversions1d",
                            "field_name": "1 Day Total Orders",
                            "field_description": "Number of attributed conversion events occurring within 24 hours of ad click.",
                            "field_type": "number",
                            "dim_met": "metric"
                          },
                          {
                            "field_id": "attributedSales1d",
                            "field_name": "1 Day Total Sales",
                            "field_description": "Total value of sales occurring within 24 hours of an ad click.",
                            "field_type": "currency",
                            "dim_met": "metric"
                          },
                          {
                            "field_id": "attributedUnitsOrdered1d",
                            "field_name": "1 Day Total Units",
                            "field_description": "Total number of units ordered within 24 hours of an ad click.",
                            "field_type": "number",
                            "dim_met": "metric"
                          },
                          {
                            "field_id": "attributedConversions14dSameSKU",
                            "field_name": "14 Day Advertised SKU Orders",
                            "field_description": "Number of attributed conversion events occurring within 14 days of ad click where the purchased SKU was the same as the SKU advertised.",
                            "field_type": "number",
                            "dim_met": "metric"
                          },
                          {
                            "field_id": "attributedSales14dSameSKU",
                            "field_name": "14 Day Advertised SKU Sales",
                            "field_description": "Total value of sales occurring within 14 days of ad click where the purchased SKU was the same as the SKU advertised.",
                            "field_type": "currency",
                            "dim_met": "metric"
                          }
                        ],
                        "default_dimension": "campaignName",
                        "default_metric": "cost"
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/query": {
      "post": {
        "operationId": "executeQuery",
        "tags": [
          "Query"
        ],
        "summary": "Run a data query",
        "description": "Returns up to `limit` rows for the requested fields over the requested date range. Supports filters, settings and period-over-period comparison via compare_to. Pagination is via `cursor`. Cursors are bound to the exact query+account; mutating any input invalidates the cursor and the response is treated as a fresh first page. Numeric values that cannot be computed (for example a ratio with a zero denominator) are returned as null. An error with `error_code: PROVIDER_UNAVAILABLE` means the platform behind the data source is throttling requests or failing temporarily: it is not an error of the client, and the same request should be retried later.",
        "x-mcp-description": "Use this tool to retrieve marketing data. Compose a request with: integration_id, connection_key, account_id, fields (array of field_ids from /v1/fields), date_range (preset or custom), optional filters, optional compare_to for period-over-period analysis, and a limit. Results come back row-by-row; when compare_to is set, each row contains current, previous and change sub-objects. Always pass field_ids and filter field IDs WITHOUT integration-specific prefixes (e.g. 'impressions', NOT 'fb:impressions'). For ga4 specifically: (1) each filter MUST include `type_of_field` (\"dimension\" or \"metric\") and may include `combination` (\"AND\"/\"OR\"); (2) all filters must share the same type_of_field; (3) temporal dimensions (date/week/month) come back in different formats depending on whether the query is trend (one of them is the only dimension of the query) or non-trend (the query has any other dimension): trend returns date='YYYY-MM-DD', month='YYYY-MM', week='YYYY-MM-DD'; non-trend returns date='YYYYMMDD', month='MM' (no year), week='WW' (no year); (4) percent metrics are returned in 0-100 scale, not 0-1.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QueryRequest"
              },
              "examples": {
                "facebook_ads": {
                  "summary": "Facebook Ads (no data_view)",
                  "value": {
                    "integration_id": "facebook_ads",
                    "connection_key": "consumer@example.com",
                    "account_id": "0000000000000000",
                    "fields": [
                      "impressions",
                      "clicks",
                      "spend",
                      "campaign_name",
                      "day"
                    ],
                    "date_range": {
                      "preset": "lastmonth"
                    },
                    "compare_to": {
                      "preset": "previous"
                    },
                    "comparison_type": "percentage",
                    "settings": {
                      "attribution_window": "ATTRIBUTION_MODEL_VIEW_CLICK###VIEW_ATTRIBUTION_WINDOW_1D###CLICK_ATTRIBUTION_WINDOW_7D"
                    },
                    "filters": [
                      {
                        "field": "campaign.name",
                        "operator": "CONTAINS",
                        "value": "brand"
                      }
                    ],
                    "limit": 100
                  }
                },
                "google_ads": {
                  "summary": "Google Ads (data_view required, dotted field names, multi-value filters supported, CONTAINS matches a substring, LIKE takes % wildcards)",
                  "value": {
                    "integration_id": "google_ads",
                    "connection_key": "user@example.com",
                    "account_id": "1234567890",
                    "data_view": "campaign",
                    "fields": [
                      "campaign.name",
                      "metrics.clicks",
                      "metrics.impressions",
                      "metrics.cost_micros",
                      "segments.date"
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 7
                    },
                    "filters": [
                      {
                        "field": "campaign.status",
                        "operator": "IN",
                        "value": [
                          "ENABLED",
                          "PAUSED"
                        ]
                      },
                      {
                        "field": "campaign.name",
                        "operator": "CONTAINS",
                        "value": "brand"
                      },
                      {
                        "field": "metrics.cost_micros",
                        "operator": "GREATER_THAN",
                        "value": 100
                      }
                    ],
                    "limit": 100
                  }
                },
                "ga4": {
                  "summary": "GA4 (dynamic filter fields, type_of_field required per filter, AND/OR per filter)",
                  "value": {
                    "integration_id": "ga4",
                    "connection_key": "user@example.com",
                    "account_id": "properties/123456789",
                    "fields": [
                      "country",
                      "sessions",
                      "activeUsers",
                      "date"
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 7
                    },
                    "filters": [
                      {
                        "field": "country",
                        "operator": "CONTAINS",
                        "value": "United",
                        "type_of_field": "dimension",
                        "combination": "AND"
                      },
                      {
                        "field": "country",
                        "operator": "CONTAINS",
                        "value": "Kingdom",
                        "type_of_field": "dimension",
                        "combination": "OR"
                      }
                    ],
                    "limit": 100
                  }
                },
                "microsoft_ads": {
                  "summary": "Microsoft Ads (data_view required; filter fields scoped to data_view, allowed_values for enums)",
                  "value": {
                    "integration_id": "microsoft_ads",
                    "connection_key": "user@example.com",
                    "account_id": "123456789",
                    "data_view": "campaign_performance",
                    "fields": [
                      "CampaignName",
                      "Impressions",
                      "Clicks",
                      "Spend",
                      "TimePeriod"
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 7
                    },
                    "filters": [
                      {
                        "field": "CampaignStatus",
                        "operator": "IN_LIST",
                        "value": [
                          "Active",
                          "Paused"
                        ]
                      },
                      {
                        "field": "CampaignName",
                        "operator": "CONTAINS",
                        "value": "Brand"
                      }
                    ],
                    "limit": 100
                  }
                },
                "youtube": {
                  "summary": "YouTube (data_view required; only IN_LIST operator; single-value-only for most fields)",
                  "value": {
                    "integration_id": "youtube",
                    "connection_key": "user@example.com",
                    "account_id": "UCxxxxxxxxxxxxxxxxxxxxxxx",
                    "data_view": "traffic_sources",
                    "fields": [
                      "day",
                      "insightTrafficSourceType",
                      "views",
                      "estimatedMinutesWatched"
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "filters": [
                      {
                        "field": "country",
                        "operator": "IN_LIST",
                        "value": [
                          "ES"
                        ]
                      },
                      {
                        "field": "subscribedStatus",
                        "operator": "IN_LIST",
                        "value": [
                          "SUBSCRIBED"
                        ]
                      },
                      {
                        "field": "insightTrafficSourceType",
                        "operator": "IN_LIST",
                        "value": [
                          "YT_SEARCH"
                        ]
                      }
                    ],
                    "limit": 100
                  }
                },
                "google_search_console": {
                  "summary": "Google Search Console (no data_view; account_id is the site URL; setting search_type; single-value filters, with MATCHES_REGEX on page and query to select several values at once; max 1 dimension and >=1 metric in fields[])",
                  "value": {
                    "integration_id": "google_search_console",
                    "connection_key": "user@example.com",
                    "account_id": "https://example.com/",
                    "fields": [
                      "query",
                      "clicks",
                      "impressions",
                      "ctr",
                      "avgposition"
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "filters": [
                      {
                        "field": "country",
                        "operator": "EQUALS",
                        "value": "usa"
                      },
                      {
                        "field": "device",
                        "operator": "EQUALS",
                        "value": "DESKTOP"
                      },
                      {
                        "field": "query",
                        "operator": "MATCHES_REGEX",
                        "value": "^(first term|second term)$"
                      }
                    ],
                    "settings": {
                      "search_type": "web"
                    },
                    "limit": 100
                  }
                },
                "google_business_profile": {
                  "summary": "Google Business Profile (data_view required; account_id is the full 'accounts/X/locations/Y' path; filters NOT supported; at least 1 metric required; max 1 dimension for performance)",
                  "value": {
                    "integration_id": "google_business_profile",
                    "connection_key": "user@example.com",
                    "account_id": "accounts/12345/locations/67890",
                    "data_view": "performance",
                    "fields": [
                      "day",
                      "business_impressions_total_calc",
                      "website_clicks",
                      "business_direction_requests"
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 100
                  }
                },
                "facebook_insights": {
                  "summary": "Facebook Insights (data_view required; account_id is the plain page_id; filters NOT supported; post/reel require dim+metric; aggregated data_views accept only metrics)",
                  "value": {
                    "integration_id": "facebook_insights",
                    "connection_key": "user@example.com",
                    "account_id": "123456789012345",
                    "data_view": "page",
                    "fields": [
                      "day",
                      "page_media_view"
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 28
                  }
                },
                "instagram_insights": {
                  "summary": "Instagram Insights (data_view required; account_id is the plain ig_user_id; filters NOT supported; posts require dim+metric; posts_aggregated accepts only metrics",
                  "value": {
                    "integration_id": "instagram_insights",
                    "connection_key": "user@example.com",
                    "account_id": "17841407839256743",
                    "data_view": "account",
                    "fields": [
                      "followers_count_metadata"
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 1
                  }
                },
                "linkedin_ads": {
                  "summary": "LinkedIn Ads (data_view required; account_id is the plain ad account numeric id; filters supported with combination='or' only -- AND is rejected; type_of_field='dimension' is REQUIRED in each filter; enum fields validate value against allowed_values; multi-value filters use comma-separated string)",
                  "value": {
                    "integration_id": "linkedin_ads",
                    "connection_key": "user@example.com",
                    "account_id": "123456789",
                    "data_view": "campaign",
                    "fields": [
                      "campaignId",
                      "impressions",
                      "clicks"
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "filters": [
                      {
                        "field": "campaignStatus",
                        "operator": "IN",
                        "value": "ACTIVE,PAUSED",
                        "type_of_field": "dimension",
                        "combination": "OR"
                      }
                    ],
                    "limit": 100
                  }
                },
                "linkedin_pages": {
                  "summary": "LinkedIn Pages (data_view required; account_id is the EXACT URN urn:li:organization:N from /v1/connections; filters NOT supported; lifetime_followers requires exactly 1 dim + 1 metric; specific_shares post-level mode requires the mandatory postText dimension; date_range is required by the generic /v1/query validation even for lifetime/realtime data_views where it is silently ignored by the executor)",
                  "value": {
                    "integration_id": "linkedin_pages",
                    "connection_key": "user@example.com",
                    "account_id": "urn:li:organization:2414183",
                    "data_view": "lifetime_followers",
                    "fields": [
                      "region",
                      "organicFollowerCount"
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 30
                    },
                    "limit": 50
                  }
                },
                "tiktok_ads": {
                  "summary": "TikTok Ads (data_view required; account_id is the plain advertiser_id; filters supported with combination='or' only -- AND is rejected; type_of_field='dimension' is REQUIRED in each filter; 10 enum fields validate value against allowed_values; multi-value filters use comma-separated string)",
                  "value": {
                    "integration_id": "tiktok_ads",
                    "connection_key": "user@example.com",
                    "account_id": "6977519744471597058",
                    "data_view": "campaign",
                    "fields": [
                      "campaign_id",
                      "impressions",
                      "clicks"
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "filters": [
                      {
                        "field": "campaign_operation_status",
                        "operator": "IN",
                        "value": "ENABLE",
                        "type_of_field": "dimension",
                        "combination": "OR"
                      }
                    ],
                    "limit": 100
                  }
                },
                "tiktok_organic": {
                  "summary": "TikTok Organic (data_view required; account_id is the plain TikTok Business id from /v1/connections; filters NOT supported; audience requires exactly 1 dim + 1 metric; video requires at least 1 dim + 1 metric)",
                  "value": {
                    "integration_id": "tiktok_organic",
                    "connection_key": "user@example.com",
                    "account_id": "<accountId>",
                    "data_view": "profile",
                    "fields": [
                      "day",
                      "profile_video_views"
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 28
                  }
                },
                "pinterest_ads": {
                  "summary": "Pinterest Ads (data_view required; account_id is the plain Pinterest ad_account_id; 4 settings strictly validated -- defaults injected if absent; filters supported with combination='or' only -- AND is rejected; type_of_field='dimension' is REQUIRED in each filter; 8 enum fields validate value against allowed_values; multi-value filters use comma-separated string; cross-setting combinations may be rejected by Pinterest with a clear executor message)",
                  "value": {
                    "integration_id": "pinterest_ads",
                    "connection_key": "user@example.com",
                    "account_id": "<ad_account_id>",
                    "data_view": "campaign",
                    "fields": [
                      "CAMPAIGN_NAME",
                      "IMPRESSION_1",
                      "CLICKTHROUGH_1",
                      "SPEND_IN_DOLLAR"
                    ],
                    "filters": [
                      {
                        "field": "CAMPAIGN_STATUS",
                        "operator": "IN",
                        "value": "ACTIVE,PAUSED",
                        "type_of_field": "dimension",
                        "combination": "OR"
                      }
                    ],
                    "settings": {
                      "click_window": "30",
                      "engagement_window": "30",
                      "view_window": "1",
                      "conversion_report_time": "TIME_OF_AD_ACTION"
                    },
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 28
                  }
                },
                "pinterest_organic": {
                  "summary": "Pinterest Organic (data_view required; account_id is the Pinterest user account id from /v1/connections; filters NOT supported; 3 declarative settings strictly validated -- pinterest_sort_by uses options_by_data_view with 5 values for top_pins and 9 video-specific values for top_video_pins; settings outside applies_to_data_views are silently ignored)",
                  "value": {
                    "integration_id": "pinterest_organic",
                    "connection_key": "user@example.com",
                    "account_id": "<user_account_id>",
                    "data_view": "top_pins",
                    "fields": [
                      "TITLE",
                      "IMPRESSION",
                      "OUTBOUND_CLICK",
                      "SAVE"
                    ],
                    "settings": {
                      "pinterest_source": "YOUR_PINS",
                      "pinterest_claimed_content": "BOTH",
                      "pinterest_sort_by": "ENGAGEMENT"
                    },
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 28
                  }
                },
                "snapchat_ads": {
                  "summary": "Snapchat Ads (data_view required; account_id is the Snapchat ad_account_id UUID; 3 settings strictly validated -- defaults injected ALWAYS; filters supported with combination='or' only -- AND rejected; type_of_field='dimension' REQUIRED; 13 enum fields validate value against allowed_values; multi-value filters use comma-separated string; view_window 'none' is lowercase by design)",
                  "value": {
                    "integration_id": "snapchat_ads",
                    "connection_key": "user@example.com",
                    "account_id": "<ad_account_id_uuid>",
                    "data_view": "campaign",
                    "fields": [
                      "campaign_name",
                      "impressions",
                      "spend",
                      "swipes"
                    ],
                    "filters": [
                      {
                        "field": "campaign_status",
                        "operator": "IN",
                        "value": "ACTIVE",
                        "type_of_field": "dimension",
                        "combination": "OR"
                      }
                    ],
                    "settings": {
                      "click_window": "28_DAY",
                      "view_window": "1_DAY",
                      "conversion_report_time": "TIME_OF_IMPRESSION"
                    },
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 28
                  }
                },
                "reddit_ads": {
                  "summary": "Reddit Ads (data_view required; account_id is the Reddit ad_account_id from /v1/connections; settings=null; filters supported ONLY with POSITIVE operators -- EQUALS for ids, CONTAINS for names, IN for enums; NOT_EQUALS/NOT_CONTAINS/NOT_IN rejected; combination='or' only -- AND rejected; type_of_field='dimension' REQUIRED; allowed_values shared 11-value list across the 3 effective_status enums)",
                  "value": {
                    "integration_id": "reddit_ads",
                    "connection_key": "user@example.com",
                    "account_id": "<ad_account_id>",
                    "data_view": "campaign",
                    "fields": [
                      "campaign_name",
                      "impressions",
                      "spend"
                    ],
                    "filters": [
                      {
                        "field": "campaign_effective_status",
                        "operator": "IN",
                        "value": "ACTIVE",
                        "type_of_field": "dimension",
                        "combination": "OR"
                      }
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 28
                  }
                },
                "twitter_analytics": {
                  "summary": "Twitter Analytics (data_view NOT required; connection_key='@username'; account_id is the Twitter user_id; single profile-lifetime scope; fields[] requires >=1 metric; pseudo-dimension `account` optional)",
                  "value": {
                    "integration_id": "twitter_analytics",
                    "connection_key": "@reportingninja",
                    "account_id": "<twitter_user_id>",
                    "fields": [
                      "followers",
                      "tweets",
                      "following"
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 28
                  }
                },
                "twitter_ads": {
                  "summary": "Twitter Ads (data_view required; account_id is the Twitter Ads alphanumeric id from /v1/connections; settings.placement REQUIRED -- ALL_ON_TWITTER default; filters supported with combination='and' only -- OR rejected; type_of_field='dimension' REQUIRED; no enums; native API async via job-queue but exposed as sync -- prefer date_range <=28d)",
                  "value": {
                    "integration_id": "twitter_ads",
                    "connection_key": "user@example.com",
                    "account_id": "<twitter_ads_account_id>",
                    "data_view": "campaign",
                    "fields": [
                      "campaignName",
                      "impressions",
                      "billedChargeLocalMicro"
                    ],
                    "filters": [
                      {
                        "field": "campaignName",
                        "operator": "CONTAINS",
                        "value": "summer",
                        "type_of_field": "dimension",
                        "combination": "AND"
                      }
                    ],
                    "settings": {
                      "placement": "ALL_ON_TWITTER"
                    },
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 7
                    },
                    "limit": 28
                  }
                },
                "mailchimp_campaign_overview": {
                  "summary": "Mailchimp (entity=campaign + data_view=campaign_overview; settings.entity REQUIRED no default; filters NOT supported; field ids WITHOUT email_ prefix; ALL_CAMPAIGNS_SENT_IN_DATE_RANGE valid account_id only with data_view=campaign_overview; dates ISO yyyy-MM-dd; percentages raw 0-100)",
                  "value": {
                    "integration_id": "mailchimp",
                    "connection_key": "user@example.com",
                    "account_id": "<campaign_id_or_ALL_CAMPAIGNS_SENT_IN_DATE_RANGE>",
                    "data_view": "campaign_overview",
                    "fields": [
                      "campaignName",
                      "emailsSent",
                      "opens",
                      "clicks"
                    ],
                    "settings": {
                      "entity": "campaign"
                    },
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 100
                  }
                },
                "mailchimp_list_overview": {
                  "summary": "Mailchimp (entity=list + data_view=list_overview)",
                  "value": {
                    "integration_id": "mailchimp",
                    "connection_key": "user@example.com",
                    "account_id": "<list_id>",
                    "data_view": "list_overview",
                    "fields": [
                      "listName",
                      "memberCount"
                    ],
                    "settings": {
                      "entity": "list"
                    },
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 100
                  }
                },
                "cross_platform": {
                  "summary": "Cross-platform (no data_view; no settings; no filters; account_id MUST equal connection_key; field ids WITHOUT cx: prefix; standard catalog of 19 fields)",
                  "value": {
                    "integration_id": "cross_platform",
                    "connection_key": "<group_uuid>",
                    "account_id": "<group_uuid>",
                    "fields": [
                      "account",
                      "impressions",
                      "clicks",
                      "cost",
                      "ROAS"
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 100
                  }
                },
                "cross_platform_campaigns": {
                  "summary": "Cross-platform at campaign level, with a campaign name dimension of the accounts group (derived_<id>, from /v1/fields with include_custom_fields=true)",
                  "value": {
                    "integration_id": "cross_platform",
                    "connection_key": "<group_uuid>",
                    "account_id": "<group_uuid>",
                    "fields": [
                      "platform",
                      "campaignName",
                      "campaignStatus",
                      "derived_<id>",
                      "cost",
                      "conversions"
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 100
                  }
                },
                "hubspot_analytics_sources": {
                  "summary": "HubSpot analytics_sources with multi-select sources setting (API renames array to comma-separated `sources` internally)",
                  "value": {
                    "integration_id": "hubspot",
                    "connection_key": "<hubspot_credential_id>",
                    "account_id": "<hubspot_credential_id>",
                    "data_view": "analytics_sources",
                    "fields": [
                      "sessions",
                      "newVisitorSessions",
                      "bounceRate"
                    ],
                    "settings": {
                      "analytics_view": "<view_id_from_v1_connections>",
                      "analytics_sources": [
                        "direct",
                        "organic"
                      ]
                    },
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 100
                  }
                },
                "hubspot_contacts": {
                  "summary": "HubSpot contacts with lifecycle_stage + queried_date + owner settings (note: queried_date for contacts has different options than for deals; sending a setting outside its applies_to_data_views is silently ignored)",
                  "value": {
                    "integration_id": "hubspot",
                    "connection_key": "<hubspot_credential_id>",
                    "account_id": "<hubspot_credential_id>",
                    "data_view": "contacts",
                    "fields": [
                      "contactsCount",
                      "newContactsCount"
                    ],
                    "settings": {
                      "queried_date": "hs_createdate",
                      "lifecycle_stage": "lead",
                      "owner": "<owner_id_from_v1_connections>"
                    },
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 100
                  }
                },
                "hubspot_deals": {
                  "summary": "HubSpot deals with pipeline_stage + queried_date settings (pipeline_stage format: `pipelineId###stageId`)",
                  "value": {
                    "integration_id": "hubspot",
                    "connection_key": "<hubspot_credential_id>",
                    "account_id": "<hubspot_credential_id>",
                    "data_view": "deals",
                    "fields": [
                      "dealCount",
                      "dealAmount"
                    ],
                    "settings": {
                      "queried_date": "hs_closedate",
                      "pipeline_stage": "<pipelineId>###<stageId>",
                      "owner": "<owner_id_from_v1_connections>"
                    },
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 100
                  }
                },
                "klaviyo": {
                  "summary": "Klaviyo (data_view required; account_id is the Klaviyo account id from /v1/connections -- one account per connection; settings=null; filters supported with combination='or' only -- AND rejected; type_of_field='dimension' REQUIRED; operators per category: EQUALS/NOT_EQUALS for ids, CONTAINS/NOT_CONTAINS/EQUALS/NOT_EQUALS/BEGINS_WITH for names, IN/NOT_IN for enums; enum values are case-sensitive against allowed_values; IN/NOT_IN accept an array or a comma-separated string; the day time dimension is limited to 60-day ranges on flow/segment/form; list is a snapshot that ignores the date range)",
                  "value": {
                    "integration_id": "klaviyo",
                    "connection_key": "<klaviyo_account_id>",
                    "account_id": "<klaviyo_account_id>",
                    "data_view": "campaign",
                    "fields": [
                      "campaign_name",
                      "delivered",
                      "open_rate",
                      "conversion_value"
                    ],
                    "filters": [
                      {
                        "field": "campaign_channel",
                        "operator": "IN",
                        "value": "email,sms",
                        "type_of_field": "dimension",
                        "combination": "OR"
                      }
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 28
                  }
                },
                "shopify": {
                  "summary": "Shopify (data_view required; connection_key and account_id are both the shop id from /v1/connections -- one store per connection; settings=null; filters supported with a homogeneous combination -- 'AND' or 'OR' (default OR), the same on every condition; type_of_field='dimension' REQUIRED; operators per category: EQUALS/NOT_EQUALS for ids, CONTAINS/NOT_CONTAINS/EQUALS/NOT_EQUALS/BEGINS_WITH for names, IN/NOT_IN for enums; enum values are case-sensitive against allowed_values; IN/NOT_IN accept an array or a comma-separated string; date ranges are limited to 366 days and orders older than 60 days require the read_all_orders scope on the connected app; return metrics are attributed to the refund date; inventory is a snapshot that ignores the date range; money metrics come in the store currency)",
                  "value": {
                    "integration_id": "shopify",
                    "connection_key": "<shop_id>",
                    "account_id": "<shop_id>",
                    "data_view": "orders",
                    "fields": [
                      "day",
                      "orders",
                      "total_sales",
                      "average_order_value"
                    ],
                    "filters": [
                      {
                        "field": "financial_status",
                        "operator": "IN",
                        "value": "PAID,PARTIALLY_REFUNDED",
                        "type_of_field": "dimension",
                        "combination": "OR"
                      }
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 28
                  }
                },
                "chatgpt_ads": {
                  "summary": "ChatGPT Ads (data_view required; connection_key and account_id are both the ad account id from /v1/connections -- one ad account per connection; settings=null; filters supported with a homogeneous combination -- 'AND' or 'OR' (default OR), the same on every condition; type_of_field='dimension' REQUIRED; operators per category: EQUALS/NOT_EQUALS for ids, CONTAINS/NOT_CONTAINS/EQUALS/NOT_EQUALS/BEGINS_WITH for names, IN/NOT_IN for enums; enum values are case-sensitive lowercase; IN/NOT_IN accept an array or a comma-separated string; date ranges are limited to 366 days; conversion metrics are period totals -- not available with the day, week or month dimensions, nor with the country, device and product dimensions; one segment family per query; money metrics come in the ad account currency; percent metrics in 0-100 scale)",
                  "value": {
                    "integration_id": "chatgpt_ads",
                    "connection_key": "<ad_account_id>",
                    "account_id": "<ad_account_id>",
                    "data_view": "campaign",
                    "fields": [
                      "day",
                      "campaign_name",
                      "spend",
                      "impressions",
                      "clicks",
                      "ctr"
                    ],
                    "filters": [
                      {
                        "field": "campaign_status",
                        "operator": "IN",
                        "value": [
                          "active",
                          "paused"
                        ],
                        "type_of_field": "dimension",
                        "combination": "OR"
                      }
                    ],
                    "date_range": {
                      "preset": "lastxdays",
                      "x": 28
                    },
                    "limit": 100
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer of the call, or a business error envelope. This endpoint reports its own errors with HTTP 200 and status=error: read envelope.status before reading data.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/QueryResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    }
                  ]
                },
                "examples": {
                  "ok_no_compare": {
                    "summary": "Without comparison",
                    "value": {
                      "status": "ok",
                      "data": {
                        "rows": [
                          {
                            "day": "2026-04-30",
                            "campaign_name": "Brand ES",
                            "impressions": 12340,
                            "clicks": 890,
                            "spend": 234.5
                          }
                        ]
                      },
                      "meta": {
                        "total_rows": 847,
                        "returned_rows": 100,
                        "has_more": true,
                        "next_cursor": "eyJoIjoiYWJjIiwiYSI6IjEyMyIsIm8iOjEwMH0=",
                        "date_range_resolved": {
                          "start": "2026-04-01",
                          "end": "2026-04-30"
                        },
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-16T10:30:00Z"
                      }
                    }
                  },
                  "ok_with_compare": {
                    "summary": "With period-over-period comparison",
                    "value": {
                      "status": "ok",
                      "data": {
                        "rows": [
                          {
                            "campaign_name": "Brand ES",
                            "current": {
                              "impressions": 12340,
                              "clicks": 890,
                              "spend": 234.5
                            },
                            "previous": {
                              "impressions": 10200,
                              "clicks": 720,
                              "spend": 210.0
                            },
                            "change": {
                              "impressions": {
                                "absolute": 2140,
                                "percentage": 20.98
                              },
                              "clicks": {
                                "absolute": 170,
                                "percentage": 23.61
                              },
                              "spend": {
                                "absolute": 24.5,
                                "percentage": 11.67
                              }
                            }
                          }
                        ]
                      },
                      "meta": {
                        "total_rows": 24,
                        "returned_rows": 24,
                        "has_more": false,
                        "date_range_resolved": {
                          "start": "2026-04-01",
                          "end": "2026-04-30"
                        },
                        "compare_date_range_resolved": {
                          "start": "2026-03-01",
                          "end": "2026-03-31"
                        },
                        "comparison_type": "percentage",
                        "request_id": "req_abc123",
                        "timestamp": "2026-05-16T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/query/async": {
      "post": {
        "summary": "Submit an async query",
        "description": "Validates the input identically to /v1/query. On success persists the query as a background job and returns a job_id immediately. The query is processed asynchronously; poll /v1/query/status to retrieve the result. Successful results are retained for 8 hours after completion. Authorization: Bearer token (same as other endpoints).",
        "operationId": "submitAsyncQuery",
        "tags": [
          "Query"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QueryRequest"
              },
              "examples": {
                "facebook_ads": {
                  "summary": "The same body /v1/query takes. The answer is a job_id, not the rows.",
                  "value": {
                    "integration_id": "facebook_ads",
                    "connection_key": "consumer@example.com",
                    "account_id": "0000000000000000",
                    "fields": [
                      "impressions",
                      "clicks",
                      "spend",
                      "campaign_name",
                      "day"
                    ],
                    "date_range": {
                      "preset": "lastmonth"
                    },
                    "compare_to": {
                      "preset": "previous"
                    },
                    "comparison_type": "percentage",
                    "settings": {
                      "attribution_window": "ATTRIBUTION_MODEL_VIEW_CLICK###VIEW_ATTRIBUTION_WINDOW_1D###CLICK_ATTRIBUTION_WINDOW_7D"
                    },
                    "filters": [
                      {
                        "field": "campaign.name",
                        "operator": "CONTAINS",
                        "value": "brand"
                      }
                    ],
                    "limit": 100
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer of the call, or a business error envelope. This endpoint reports its own errors with HTTP 200 and status=error: read envelope.status before reading data.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/AsyncQueryAcceptedResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    }
                  ]
                },
                "example": {
                  "status": "ok",
                  "data": {
                    "job_id": "4028808ca0b31c7d01a0b31d44e70000",
                    "status": "pending"
                  },
                  "meta": {
                    "request_id": "req_a1b2c3d4e5f6",
                    "timestamp": "2026-05-25T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-mcp-description": "Use the submit_async_query tool for a pull that would not finish in one call -- a long period, a wide breakdown, a large limit. It answers a job_id right away; poll get_query_status with it until the status is success or error."
      }
    },
    "/query/status": {
      "post": {
        "summary": "Poll an async query's status",
        "description": "Returns the current state of a previously submitted async query (pending, running, success, error). When status is `success`, the original /v1/query response body is embedded under data.result with the same shape (status + data.rows[] + meta). When status is `error`, data.error_message contains the failure reason. Authorization: same Bearer token. A job_id belonging to another account returns the same `job_id not found or expired` envelope as unknown ids. Successful results are retained for 8 hours; after that the job returns the same not-found envelope.",
        "operationId": "queryStatus",
        "tags": [
          "Query"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QueryStatusRequest"
              },
              "examples": {
                "poll": {
                  "summary": "The job_id /v1/query/async answered.",
                  "value": {
                    "job_id": "4028808ca0b31c7d01a0b31d44e70000"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer of the call, or a business error envelope. This endpoint reports its own errors with HTTP 200 and status=error: read envelope.status before reading data.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/AsyncQueryStatusResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    }
                  ]
                },
                "example": {
                  "status": "ok",
                  "data": {
                    "job_id": "4028808ca0b31c7d01a0b31d44e70000",
                    "status": "success",
                    "created_at": 1716639000000,
                    "started_at": 1716639020000,
                    "completed_at": 1716639060000,
                    "result": {
                      "status": "ok",
                      "data": {
                        "rows": [
                          {
                            "day": "2026-04-30",
                            "impressions": 12340
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-05-25T10:30:00Z",
                        "total_rows": 1,
                        "returned_rows": 1,
                        "has_more": false
                      }
                    }
                  },
                  "meta": {
                    "request_id": "req_9z8y7x6w5v4u",
                    "timestamp": "2026-05-25T10:31:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-mcp-description": "Use the get_query_status tool to poll the job_id that submit_async_query returned. While the status is pending or running there is nothing to read; on success the whole query answer is in result."
      }
    },
    "/reports/list": {
      "post": {
        "operationId": "listReports",
        "tags": [
          "Reports"
        ],
        "summary": "List the reports of the account",
        "description": "Returns every report of the account, the ones built on one of its templates included, ordered by title. Each row carries the key the other endpoints address it with, the version its structure is at and the report level bindings it reads from. The account's own templates are a different list and are answered by /v1/templates/list; the system catalog is a third one and is answered by /v1/catalog/list.",
        "x-mcp-description": "Use the list_reports tool to find the report_key of a report before reading it, extending it, scheduling it or deleting it. It also tells you which integrations each report is already bound to.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportsListRequest"
              },
              "examples": {
                "list": {
                  "summary": "The endpoint reads no parameters.",
                  "value": {}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportsListResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "Two reports, one of them built on a template.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "reports": [
                          {
                            "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                            "name": "Meta Ads -- monthly",
                            "internal_name": "Meta Ads -- monthly",
                            "subtitle": "Paid media performance",
                            "internal_notes": null,
                            "type": "report",
                            "based_on_template_key": null,
                            "catalog": null,
                            "created_via": "api",
                            "version": 7,
                            "created_at": "2026-08-04T09:12:41Z",
                            "report_level_data_sources": true,
                            "data_sources": [
                              {
                                "integration_id": "facebook_ads",
                                "connection_key": "3d5e8a1c72b04f96ad3c1e70b542986f",
                                "account_id": "act_412298370015"
                              }
                            ],
                            "share_link_enabled": false,
                            "share_link_id": null,
                            "share_url": null,
                            "app_url": "https://app.reportingninja.com/#!report/9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                            "share_link_password_set": false,
                            "share_link_allow_date_range": false
                          },
                          {
                            "report_key": "4d81b0e6c5a74f3298ad6e2b9c105f37",
                            "name": "Client dashboard",
                            "internal_name": "Client dashboard",
                            "subtitle": null,
                            "internal_notes": null,
                            "type": "template_based",
                            "based_on_template_key": "b27e5f4a91c34d87ae06f3d5218c9b40",
                            "catalog": {
                              "slug": "cross-platform-ads",
                              "version": 1
                            },
                            "created_via": "ui",
                            "version": 2,
                            "created_at": "2026-06-21T15:40:03Z",
                            "report_level_data_sources": true,
                            "data_sources": [
                              {
                                "integration_id": "google_ads",
                                "connection_key": "6e2a90c3b15d47f8a0d9c46e81375b2a",
                                "account_id": "574-118-2096"
                              }
                            ],
                            "share_link_enabled": true,
                            "share_link_id": "K7mQ2xR9tB4vC1nL8pZ3jW6yH5dF0sA7gE2uT9iO4kN1bV6cXM",
                            "share_url": "https://app.reportingninja.com/share/K7mQ2xR9tB4vC1nL8pZ3jW6yH5dF0sA7gE2uT9iO4kN1bV6cXM",
                            "app_url": "https://app.reportingninja.com/#!report/4d81b0e6c5a74f3298ad6e2b9c105f37",
                            "share_link_password_set": false,
                            "share_link_allow_date_range": false
                          }
                        ],
                        "count": 2
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/reports/trash": {
      "post": {
        "operationId": "listTrash",
        "tags": [
          "Reports"
        ],
        "summary": "List the reports in the trash",
        "description": "Returns the reports of the account that /v1/reports/delete put in the trash, most recently deleted first. Each row carries the same fields every report listing carries -- with the type the report had before it was deleted -- plus when it was deleted and until when /v1/reports/restore can bring it back. These reports are in none of the other listings and they do not count against the plan. A report whose restorable_until has passed is deleted for good and stops appearing here.",
        "x-mcp-description": "Use the list_trash tool to see which reports were deleted and can still be restored, and how long is left on each one. restore_report takes the report_key it answers.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportsTrashRequest"
              },
              "examples": {
                "list": {
                  "summary": "The endpoint reads no parameters.",
                  "value": {}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportsTrashResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "One report in the trash, with four days left on it.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "reports": [
                          {
                            "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                            "name": "Meta Ads -- monthly",
                            "internal_name": "Meta Ads -- monthly",
                            "subtitle": null,
                            "internal_notes": null,
                            "type": "report",
                            "based_on_template_key": null,
                            "catalog": null,
                            "created_via": "api",
                            "version": 7,
                            "created_at": "2026-08-04T09:12:41Z",
                            "report_level_data_sources": true,
                            "data_sources": [
                              {
                                "integration_id": "facebook_ads",
                                "connection_key": "3d5e8a1c72b04f96ad3c1e70b542986f",
                                "account_id": "act_412298370015"
                              }
                            ],
                            "share_link_enabled": false,
                            "share_link_id": null,
                            "share_url": null,
                            "app_url": null,
                            "share_link_password_set": false,
                            "share_link_allow_date_range": false,
                            "trashed_at": "2026-09-18T10:30:00Z",
                            "restorable_until": "2026-09-25T10:30:00Z"
                          }
                        ],
                        "count": 1
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/templates/list": {
      "post": {
        "operationId": "listTemplates",
        "tags": [
          "Reports"
        ],
        "summary": "List the templates of the account",
        "description": "Returns the templates the account owns, ordered by title, with the same header /v1/reports/list answers for a report. These are the templates a report of type template_based follows. They are not the system catalog, which every account shares and /v1/catalog/list publishes.",
        "x-mcp-description": "Use the list_report_templates tool for the account's OWN templates. The shared catalog of published templates is a different list and list_template_catalog answers it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TemplatesListRequest"
              },
              "examples": {
                "list": {
                  "summary": "The endpoint reads no parameters.",
                  "value": {}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplatesListResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "One template of the account.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "templates": [
                          {
                            "report_key": "b27e5f4a91c34d87ae06f3d5218c9b40",
                            "name": "Agency master template",
                            "internal_name": "Agency master template",
                            "subtitle": null,
                            "internal_notes": "The layout every client report of this agency follows.",
                            "type": "template",
                            "based_on_template_key": null,
                            "catalog": null,
                            "created_via": "ui",
                            "version": 11,
                            "created_at": "2026-02-17T08:05:22Z",
                            "report_level_data_sources": true,
                            "data_sources": [],
                            "share_link_enabled": false,
                            "share_link_id": null,
                            "share_url": null,
                            "app_url": "https://app.reportingninja.com/#!template/b27e5f4a91c34d87ae06f3d5218c9b40",
                            "share_link_password_set": false,
                            "share_link_allow_date_range": false
                          }
                        ],
                        "count": 1
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/reports/get": {
      "post": {
        "operationId": "getReport",
        "tags": [
          "Reports"
        ],
        "summary": "Read a report with its sections and widgets",
        "description": "Returns the header of the report and, section by section, its widgets: what each one is, where it reads from, and either its definition in the widget DSL or the named reason it has none. A widget that carries no data -- a text block, an image, a page break -- is never editable, and neither is one whose definition uses something the DSL cannot say. The answer is bounded at 500 widgets and is cut on a section boundary when the report has more, with truncated: true. The sections of a template_based report are its template's. It also carries `settings`, the settings window of the report -- brand template, front cover, PDF options, localization, language file, substitution variables, what the widgets show, the snapshot switch and the AI brief -- in the same shape /v1/reports/update takes it, with the names of the brand template and of the language file resolved. The uploaded file of the cover is published by name only: its content never travels here. The listings do not carry the block.",
        "x-mcp-description": "Use the get_report tool to see what a report already contains before adding to it: the section_key you need for create_widget, and the dsl of the widgets that are already there as a model for the next one.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportsGetRequest"
              },
              "examples": {
                "get": {
                  "summary": "The report to read.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportsGetResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "A report of one section with two widgets: one the DSL can express and one it cannot.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "name": "Meta Ads -- monthly",
                        "internal_name": "Meta Ads -- monthly",
                        "type": "report",
                        "based_on_template_key": null,
                        "catalog": null,
                        "created_via": "api",
                        "version": 7,
                        "created_at": "2026-08-04T09:12:41Z",
                        "report_level_data_sources": true,
                        "data_sources": [
                          {
                            "integration_id": "facebook_ads",
                            "connection_key": "3d5e8a1c72b04f96ad3c1e70b542986f",
                            "account_id": "act_412298370015"
                          }
                        ],
                        "share_link_enabled": false,
                        "share_link_id": null,
                        "share_url": null,
                        "app_url": "https://app.reportingninja.com/#!report/9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "share_link_password_set": false,
                        "share_link_allow_date_range": false,
                        "settings": {
                          "brand_template": "5c1a9e73d6b24fa08e5713bc9a40d2f1",
                          "brand_template_name": "Acme -- corporate",
                          "front_cover": {
                            "type": "report",
                            "title": "Meta Ads -- monthly",
                            "subtitle": "Prepared for Acme",
                            "date_line": "current_period",
                            "file": null
                          },
                          "pdf": {
                            "table_of_contents": true,
                            "page_numbers": true,
                            "title_in_pdf": false
                          },
                          "localization": {
                            "use": "report",
                            "date_format": "DDMMYYYY",
                            "week_starts_on": "monday",
                            "decimal_separator": ",",
                            "currency": "EUR",
                            "currency_symbol_position": "right",
                            "pdf_page_size": "A4"
                          },
                          "language_file": "language_file_es",
                          "language_file_name": "Spanish (system defined)",
                          "substitution_variables": [
                            "Acme",
                            "Q3",
                            "",
                            "",
                            ""
                          ],
                          "widgets_show": {
                            "data_source_name": true,
                            "data_source_icon": true,
                            "date_range": false
                          },
                          "snapshots_with_scheduled_executions": true,
                          "ai_brief": "Acme sells industrial coffee machines to hotels; the client cares about cost per lead."
                        },
                        "sections": [
                          {
                            "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                            "name": "Performance",
                            "show_previous_period": true,
                            "add_summary": {
                              "position": "last"
                            },
                            "show_date_range": true,
                            "page_orientation": "portrait",
                            "page_break": true,
                            "widgets": [
                              {
                                "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                                "name": "Spend vs conversions",
                                "row": 1,
                                "column": 1,
                                "type": "trend",
                                "integration_id": "facebook_ads",
                                "data_view": "account",
                                "editable": true,
                                "dsl": {
                                  "name": "Spend vs conversions",
                                  "visualization": "graph_trend",
                                  "chart_type": "area",
                                  "full_width": true,
                                  "content": null,
                                  "data_view": "account",
                                  "metrics": [
                                    "spend",
                                    "actions_offsite_conversion"
                                  ],
                                  "dimensions": [],
                                  "filter_key": null,
                                  "compare": null,
                                  "filters": null,
                                  "sort_by": null,
                                  "limit": null,
                                  "date_range": null,
                                  "previous_period": true,
                                  "show_change": null,
                                  "settings": null,
                                  "unfulfilled": []
                                }
                              },
                              {
                                "widget_key": "0d6b83f1e4a24c7590bd2e18c07a5f39",
                                "name": "Notes for the client",
                                "row": 2,
                                "column": 1,
                                "type": "text",
                                "integration_id": null,
                                "data_view": null,
                                "editable": true,
                                "dsl": {
                                  "name": "Notes for the client",
                                  "visualization": "text",
                                  "chart_type": null,
                                  "full_width": true,
                                  "content": "This section covers **paid media only**. Organic traffic is reported separately.",
                                  "data_view": "",
                                  "metrics": [],
                                  "dimensions": [],
                                  "filter_key": null,
                                  "compare": null,
                                  "filters": null,
                                  "sort_by": null,
                                  "limit": null,
                                  "date_range": null,
                                  "previous_period": null,
                                  "show_change": null,
                                  "settings": null,
                                  "unfulfilled": []
                                }
                              }
                            ]
                          }
                        ],
                        "widget_count": 2,
                        "editable_count": 2,
                        "truncated": false,
                        "unreadable_section_keys": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/widgets/get": {
      "post": {
        "operationId": "getWidget",
        "tags": [
          "Widgets"
        ],
        "summary": "Read one widget",
        "description": "Returns one widget of one section of a report: the same object /v1/reports/get publishes for it -- what it is, where it reads from and either its definition in the widget DSL or the named reason it has none -- plus the three things an edit needs. widget_hash is the fingerprint of the definition and travels back as if_widget_hash on a write. lost_properties says what a rewrite carrying that same definition would not be able to keep. backup_keys lists the backups the widget can be put back from, newest first; they expire after 7 days. In a report built on a template this answers for the copy the report keeps of its own, which is the widget_key /v1/reports/get publishes there.\n\nRefusals: 400 for the body, 404 NOT_FOUND for the report, the section or the widget, 409 REPORT_IN_TRASH.",
        "x-mcp-description": "Use the get_widget tool right before update_widget: it hands you the dsl to start from and the widget_hash to send back as if_widget_hash, which is what stops two writers from overwriting each other.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WidgetsGetRequest"
              },
              "examples": {
                "get": {
                  "summary": "The widget to read.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                    "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WidgetsGetResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "A widget the DSL can express, with one backup.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                        "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                        "widget": {
                          "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                          "name": "Spend by campaign",
                          "row": 2,
                          "column": 1,
                          "type": "table",
                          "integration_id": "facebook_ads",
                          "data_view": "campaign",
                          "editable": true
                        },
                        "widget_hash": "4c1f0b6e9a7d2358c04e1b9f7a3d6521e8b04c7f19a2d3586e0f4b17c92da638",
                        "lost_properties": [],
                        "backup_keys": [
                          {
                            "backup_key": "b41d7e9032a84c15bf6e0d29c73a815e",
                            "created_at": "2026-09-19T08:14:02Z"
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-20T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/catalog/list": {
      "post": {
        "operationId": "listCatalog",
        "tags": [
          "Catalog"
        ],
        "summary": "List the published template catalog",
        "description": "Returns the published templates a report can be created from with /v1/reports/create-from-template, and whose sections a composition can copy. The catalog is the same for every account. With include_sections the entry also carries the 0-based position and the name of each of its sections -- what a catalog section of a composition names -- and the parameters a parameterized entry asks a value for. A parameterized entry can be materialized whole and cannot be used as a single section of a composition.",
        "x-mcp-description": "Use the list_template_catalog tool to find a ready-made report before building one from scratch: the slug goes to create_report_from_template, and with include_sections the positions go to create_report as catalog sections.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CatalogListRequest"
              },
              "examples": {
                "defaults": {
                  "summary": "English names, without the sections of each entry.",
                  "value": {}
                },
                "with_sections": {
                  "summary": "Spanish names, with the sections and the parameters of every entry.",
                  "value": {
                    "language": "es",
                    "include_sections": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatalogListResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "One entry, read with include_sections.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "templates": [
                          {
                            "slug": "cross-platform-ads",
                            "version": 1,
                            "name": "Cross-platform ads report",
                            "description": "The monthly report for a budget split between Facebook and Google (and Microsoft Advertising, if connected): the combined result, each platform's return, how each moved and which campaigns carried the money. Needs conversion tracking with values.",
                            "type": "essential",
                            "category": "ppc",
                            "required_integrations": [
                              {
                                "integration_id": "google_ads"
                              },
                              {
                                "integration_id": "facebook_ads"
                              }
                            ],
                            "optional_integrations": [
                              {
                                "integration_id": "microsoft_ads"
                              }
                            ],
                            "parameterized": false,
                            "sections": [
                              {
                                "position": 0,
                                "name": "Overview",
                                "integrations": [
                                  {
                                    "integration_id": "google_ads"
                                  }
                                ],
                                "required": true
                              },
                              {
                                "position": 1,
                                "name": "Meta Ads",
                                "integrations": [
                                  {
                                    "integration_id": "facebook_ads"
                                  }
                                ],
                                "required": false
                              }
                            ],
                            "parameters": []
                          }
                        ],
                        "count": 1
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/users/list": {
      "post": {
        "operationId": "listClientUsers",
        "tags": [
          "Users"
        ],
        "summary": "List the client users of the account",
        "description": "Returns the users who sign into the client portal, ordered by email address. The owner and the administrators of the account are not part of this surface and never appear here. No password and no session material is ever published.",
        "x-mcp-description": "Use the list_client_users tool to find the user_key of the people a portal view is shared with, before calling create_portal_view or update_portal_view.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UsersListRequest"
              },
              "examples": {
                "list": {
                  "summary": "The endpoint reads no parameters.",
                  "value": {}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsersListResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "One client user.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "users": [
                          {
                            "user_key": "8b0d3f6a94c247e5b81a6d20fc739e14",
                            "email": "ana.reyes@example.com",
                            "first_name": "Ana",
                            "last_name": "Reyes",
                            "language": "es",
                            "role": "client",
                            "created_at": "2026-05-09T11:22:07Z"
                          }
                        ],
                        "count": 1
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/schedules/list": {
      "post": {
        "operationId": "listSchedules",
        "tags": [
          "Schedules"
        ],
        "summary": "List the scheduled deliveries",
        "description": "Returns the scheduled deliveries of the account, or of one report when report_key is sent. A schedule at frequency none is kept and listed: it is a configured delivery that is stopped. A one-off delivery that has been asked for and not sent yet is not listed at all, and neither is the request of a snapshot: the key of a queued snapshot is the `schedule_key` /v1/reports/take-snapshot answered, which 409 ALREADY_QUEUED repeats, and /v1/schedules/delete accepts it even though nothing lists it. Two kinds of schedule this version cannot express are still listed and say so: uses_system_template and conditional_execution_enabled mark the ones /v1/schedules/update refuses to change.",
        "x-mcp-description": "Use the list_schedules tool to find the schedule_key of a delivery before changing or stopping it, and to check whether a report is already scheduled before calling create_schedule.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SchedulesListRequest"
              },
              "examples": {
                "all": {
                  "summary": "Every schedule of the account.",
                  "value": {}
                },
                "of_one_report": {
                  "summary": "Only the schedules of one report.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SchedulesListResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "A weekly delivery with its own email text.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "schedules": [
                          {
                            "schedule_key": "2e8b6d34fa9147c5b03e7a1c986df205",
                            "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                            "frequency": "weekly",
                            "frequency_details": "MONDAY",
                            "time": "08:00",
                            "recipients_to": [
                              "client@example.com"
                            ],
                            "recipients_cc": [],
                            "recipients_bcc": [],
                            "report_format": "pdf",
                            "email_template_key": null,
                            "uses_system_template": false,
                            "format": "html",
                            "subject": "Your weekly report",
                            "sender": "Studio Reports",
                            "sender_email": "reports@example.com",
                            "reply_to_email": "reports@example.com",
                            "origin": "api",
                            "user_key": "8b0d3f6a94c247e5b81a6d20fc739e14",
                            "conditional_execution_enabled": false,
                            "last_execution_at": "2026-09-14T06:00:11Z",
                            "queued": false
                          }
                        ],
                        "count": 1
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/portal-views/list": {
      "post": {
        "operationId": "listPortalViews",
        "tags": [
          "Portal Views"
        ],
        "summary": "List the views of the client portal",
        "description": "Returns the views of the client portal: which reports which client users see, in the order the portal menu shows them, and with which brand and which choice of periods.",
        "x-mcp-description": "Use the list_portal_views tool to find the portal_view_key of a view before changing it, and to see which reports a client already has access to.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PortalViewsListRequest"
              },
              "examples": {
                "list": {
                  "summary": "The endpoint reads no parameters.",
                  "value": {}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortalViewsListResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "One view with one report and one user.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "portal_views": [
                          {
                            "portal_view_key": "5c94a7e208f34b16bd7e03c2a951f687",
                            "name": "Acme",
                            "internal_notes": "Renewal in November",
                            "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                            "send_notification": true,
                            "allow_to_change_date_range": true,
                            "date_range_option": "last_monthly_periods",
                            "report_keys": [
                              "9f3c1a7e42b84d0596ce1f8a3b7d2e64"
                            ],
                            "user_keys": [
                              "8b0d3f6a94c247e5b81a6d20fc739e14"
                            ]
                          }
                        ],
                        "count": 1
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/capabilities": {
      "post": {
        "operationId": "getCapabilities",
        "tags": [
          "Capabilities"
        ],
        "summary": "Read what this version of the API can do",
        "description": "Returns the ceilings, the vocabularies and the guarantees this version publishes, so a client can read them instead of hard-coding them: how many sections a report takes, which shapes a widget can have, which periods a report can be built for, how long a confirmation token lives, which endpoints confirm and which accept if_version, and whether write access is enabled for the account that is calling. Everything else in this document is fixed at publication time; this endpoint is the one that answers for today.",
        "x-mcp-description": "Use the get_capabilities tool BEFORE building a report: it answers the ceilings, the widget shapes, the date presets and whether this connection may write at all.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CapabilitiesRequest"
              },
              "examples": {
                "read": {
                  "summary": "The endpoint reads no parameters.",
                  "value": {}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CapabilitiesResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "All twenty-six blocks. contract, versioning and account are complete; the rest are shown with the first few of their fields, which is where a client reads the real ceilings from.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "contract": {
                          "version": "1.5.0",
                          "version_note": "The release of the report surface. Every entry of dsl_additions_since_ga and of limit_changes_since_ga names the version that carried it, so a client that integrated against an older one knows what it has to send now.",
                          "dsl_schema_id": "widget-intent-v1",
                          "composition_schema_id": "report-compose-v1",
                          "dsl_changes_in_this_version": false,
                          "dsl_additions_since_ga": [
                            {
                              "key": "chart_type",
                              "added": "2026-09-18",
                              "version": "1.2.0",
                              "applies_to": [
                                "graph_basic",
                                "graph_trend"
                              ],
                              "note": "Required like every other property of the dsl; null asks for the default chart of the shape, and is the only value the shapes that draw no chart take."
                            },
                            {
                              "key": "full_width",
                              "added": "2026-09-19",
                              "version": "1.2.0",
                              "applies_to": [
                                "graph_basic",
                                "graph_pie",
                                "graph_trend",
                                "table_metrics_summary",
                                "table_dimensions_metrics",
                                "text"
                              ],
                              "note": "Required like every other property of the dsl; true gives the widget a row of its own and null, like false, pairs it up with the next one. A kpi ignores it."
                            },
                            {
                              "key": "content",
                              "added": "2026-09-19",
                              "version": "1.2.0",
                              "applies_to": [
                                "text"
                              ],
                              "note": "Required like every other property of the dsl, and null in every shape but the text block, which was added to the visualization enum on the same day. metrics stopped declaring a minimum of one item in the same change: a text block carries an empty array, and the six shapes that read data are refused with METRICS_REQUIRED when they carry none."
                            },
                            {
                              "key": "show_change",
                              "added": "2026-09-20",
                              "version": "1.3.0",
                              "applies_to": [
                                "kpi",
                                "table_metrics_summary"
                              ],
                              "note": "Required like every other property of the dsl. It is the PERCENTAGE of change against the period before of a kpi and of a metrics summary, and previous_period is the figure of that period beside it. In a kpi the two are independent, as the two controls of the settings window are; in a table_metrics_summary the percentage is drawn beside the figure and with previous_period false it is not added, which is what the window and the three readers of a report do. In both shapes null means YES, which is what they are drawn with by default, so a definition that names neither builds the same widget it built before the key existed; everywhere else show_change is null and is ignored with a warning. On the sources and levels where the editor greys both controls out -- a figure that is a snapshot or a lifetime total -- the widget is built with the comparison off, the answer carries a warning, and a read of such a widget describes it as off whatever its row says."
                            },
                            {
                              "key": "filter_key",
                              "added": "2026-09-22",
                              "version": "1.4.0",
                              "applies_to": [
                                "kpi",
                                "graph_basic",
                                "graph_pie",
                                "graph_trend",
                                "table_metrics_summary",
                                "table_dimensions_metrics"
                              ],
                              "note": "Required like every other property of the dsl; null is a widget that reads no saved filter, which is the usual answer. It names a filter of the account by its key, as /filters/list publishes it, and it EXCLUDES filters: a widget points at a saved filter or carries conditions of its own, never both, and a body with the two is refused with FILTER_KEY_AND_FILTERS. The filter has to belong to the integration of the widget, and to its data view on the sources whose filters are read in one. It is written by clients of this API and of MCP only: a definition being generated that carries one is refused with KEY_NOT_FOR_GENERATION. Every definition this API ANSWERS carries the key, null included, and it is one of the two properties of the dsl a body may leave out -- compare is the other -- so a client that integrated before the key existed keeps working, and its widgets read no saved filter, which is what the absent key says."
                            },
                            {
                              "key": "metrics[].filter_key",
                              "added": "2026-09-22",
                              "version": "1.4.0",
                              "applies_to": [
                                "table_metrics_summary"
                              ],
                              "note": "Required like every other property of the dsl, inside the long form of an entry of metrics[]: an entry is the field id on its own, or the object {id, filter_key} with both keys present. It reads THAT ONE COLUMN through a saved filter of the account and only a table_metrics_summary has columns of its own; anywhere else it is refused with FILTER_KEY_NOT_FOR_THIS_SHAPE, and so it is on hubspot, where that field of a column is the traffic source the widget is read by. Written by clients of this API and of MCP only."
                            },
                            {
                              "key": "compare",
                              "added": "2026-09-22",
                              "version": "1.4.0",
                              "applies_to": [
                                "graph_basic",
                                "graph_pie",
                                "graph_trend",
                                "table_metrics_summary"
                              ],
                              "note": "Required like every other property of the dsl; null is a widget that compares nothing, which is the usual answer. `by` says what is compared -- accounts, filters or breakdown -- and it is decided when the widget is CREATED: an update does not change the class of a comparator nor stop a widget being one, and a body that tries is refused with COMPARATOR_CLASS_FIXED. The axis is part of that class: over_time moves from one axis to another and never from none to an axis or back, because those are two subtypes of the wizard and not one. Which classes an integration offers depends on the ENTITY LEVEL of the data view as well as on the integration. A comparator of accounts names at least 2 of them, one item per account, and takes no connection_key of its own; a comparator of filters names at least one saved filter of the account. over_time is the axis a table_metrics_summary comparator is broken down along and show_past_serie the second series of a breakdown; the other shapes carry both as null, and the same account or the same saved filter named twice is refused. A comparator draws no comparison with the period before, so previous_period and show_change are null in one. Every definition this API ANSWERS carries the key, null included, and it is the second of the two properties a body may leave out."
                            },
                            {
                              "key": "add_summary",
                              "added": "2026-09-19",
                              "version": "1.2.0",
                              "applies_to": [
                                "section"
                              ],
                              "note": "Required like every other key of a section, and null asks for nothing. It is read in every section of /reports/create and of /reports/append, and the add_summary at the ROOT of a creation -- the summary of the whole report -- is another key and has not changed."
                            },
                            {
                              "key": "show_previous_period",
                              "added": "2026-09-19",
                              "version": "1.2.0",
                              "applies_to": [
                                "section"
                              ],
                              "note": "Required like every other key of a section, and null, like false, asks for nothing."
                            }
                          ],
                          "limit_changes_since_ga": [
                            {
                              "limit": "name_max_length",
                              "applies_to": [
                                "section",
                                "widget"
                              ],
                              "changed": "2026-09-20",
                              "version": "1.3.0",
                              "from": 255,
                              "to": 80,
                              "note": "The name of a widget and the name of a section are between 1 and 80 characters, the range the editor of the product uses. They used to be accepted up to 255, which is what the column holds and not what a report can draw. Names already stored are untouched."
                            },
                            {
                              "limit": "name_max_length",
                              "applies_to": [
                                "report"
                              ],
                              "changed": "2026-09-21",
                              "version": "1.3.0",
                              "from": 255,
                              "to": 60,
                              "note": "A report has two names and they are now bounded by what the application draws, not by the column: `name` is the TITLE, 1..60, and `internal_name` is the name of the list, 1..100. Both used to be written together from `name` alone and accepted up to 255. Names already stored are untouched, and a body that still sends only `name` keeps working: it is used for both."
                            }
                          ],
                          "problems_max": 50,
                          "problems_max_note": "A validation answer lists at most this many problems; when there are more, the last entry is TOO_MANY_PROBLEMS.",
                          "unknown_fields_rejected": true,
                          "unknown_fields_note": "A write body that carries a field the endpoint does not read is refused with UNKNOWN_PARAM and the path of the field, never written around. The fields of each section of a composition are read the same way, and inside a dsl object a property that is not one of the schema's is refused with DSL_INVALID.",
                          "no_supplementary_characters": true,
                          "no_supplementary_characters_note": "Text fields do not accept characters outside the Basic Multilingual Plane -- emojis, most often. A value that carries one is refused with INVALID_VALUE and the path of the field, the same way the application refuses it."
                        },
                        "widgets": {
                          "shapes": [
                            "kpi",
                            "graph_basic",
                            "graph_pie",
                            "graph_trend",
                            "table_metrics_summary",
                            "table_dimensions_metrics",
                            "text"
                          ],
                          "shape_rules": {
                            "kpi": {
                              "metrics_min": 1,
                              "metrics_max": 1,
                              "dimensions_min": 0,
                              "dimensions_max": 0
                            },
                            "graph_basic": {
                              "dimensions_min": 1,
                              "dimensions_max": null,
                              "dimensions_min_waived_when_seeded": true,
                              "note": "The chart is drawn along a single dimension. More than one is accepted with a warning, and mixing the time field with another dimension is refused. The data views marked dimensions_optional accept an empty list: the platform seeds the dimension."
                            },
                            "graph_pie": {
                              "dimensions_min": 1,
                              "dimensions_max": 1,
                              "dimensions_min_waived_when_seeded": true
                            },
                            "graph_trend": {
                              "dimensions_min": 0,
                              "dimensions_max": null,
                              "note": "A trend is drawn along the time axis of its source. A single time dimension is read as the granularity of the series; anything else is a warning and is replaced by the daily axis, and the order is always the time axis."
                            },
                            "text": {
                              "metrics_min": 0,
                              "metrics_max": 0,
                              "dimensions_min": 0,
                              "dimensions_max": 0,
                              "note": "The one shape that reads no data: it carries content and the empty value of every other key. See widgets.text for what the content may hold."
                            }
                          },
                          "chart_types": {
                            "graph_basic": {
                              "values": [
                                "area",
                                "line",
                                "column",
                                "bar"
                              ],
                              "default": "column",
                              "note": "Null takes the default. chart_type is read by this shape and by graph_trend only; the other four carry null. 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."
                            },
                            "graph_trend": {
                              "values": [
                                "area",
                                "line",
                                "column"
                              ],
                              "default": "area",
                              "note": "Null takes the default. A trend draws no bars, on this shape and on a chart the platform builds as one."
                            }
                          },
                          "text": {
                            "max_chars": 2000,
                            "markdown": [
                              "paragraphs",
                              "bold",
                              "bullet_list",
                              "numbered_list"
                            ],
                            "note": "A text block is prose and nothing else: it carries content and no metrics, no dimensions, no data view, no filter, no order, no limit and no range, and it takes a row of its own unless full_width says otherwise. Markdown outside the list above -- headings, links, italics, images, HTML, emoji -- is printed as the characters it is made of. A text block is written before the widgets of the report are run, so it names no figures."
                          },
                          "metrics_max": 10,
                          "dimensions_max": 10
                        },
                        "comparators": {
                          "field": "compare",
                          "shapes": [
                            "graph_basic",
                            "graph_pie",
                            "graph_trend",
                            "table_metrics_summary"
                          ],
                          "bys": [
                            "accounts",
                            "filters",
                            "breakdown"
                          ],
                          "over_time": [
                            "none",
                            "hour_of_day",
                            "day_of_week",
                            "date",
                            "week",
                            "month",
                            "year"
                          ],
                          "min_compared_accounts": 2
                        },
                        "integrations": [
                          {
                            "integration_id": "facebook_ads",
                            "dsl_available": true,
                            "dsl_data_views": [
                              {
                                "data_view": "account",
                                "shapes": [
                                  "graph_basic",
                                  "graph_pie",
                                  "graph_trend",
                                  "kpi",
                                  "table_metrics_summary",
                                  "table_dimensions_metrics"
                                ],
                                "dimensions_optional": false
                              },
                              {
                                "data_view": "campaigns",
                                "shapes": [
                                  "graph_basic",
                                  "graph_pie",
                                  "graph_trend",
                                  "kpi",
                                  "table_metrics_summary",
                                  "table_dimensions_metrics"
                                ],
                                "dimensions_optional": false
                              }
                            ],
                            "note": "Data views of the widget DSL; the query API data views are in /v1/integrations/detail. An empty data_view is the word of the DSL for a source with a single view: send it as an empty string"
                          },
                          {
                            "integration_id": "google_ads",
                            "dsl_available": true,
                            "dsl_data_views": [
                              {
                                "data_view": "customer",
                                "shapes": [
                                  "graph_basic",
                                  "graph_pie",
                                  "kpi",
                                  "table_metrics_summary",
                                  "table_dimensions_metrics",
                                  "graph_trend"
                                ],
                                "dimensions_optional": false
                              },
                              {
                                "data_view": "campaign_group",
                                "shapes": [
                                  "graph_basic",
                                  "graph_pie",
                                  "kpi",
                                  "table_metrics_summary",
                                  "table_dimensions_metrics",
                                  "graph_trend"
                                ],
                                "dimensions_optional": false
                              }
                            ],
                            "note": "Data views of the widget DSL; the query API data views are in /v1/integrations/detail. An empty data_view is the word of the DSL for a source with a single view: send it as an empty string"
                          }
                        ],
                        "sources_without_dsl": [
                          {
                            "integration_id": null,
                            "integration": "Microsoft Excel (.xlsx)"
                          },
                          {
                            "integration_id": null,
                            "integration": "Google Sheets"
                          }
                        ],
                        "query_not_available": {
                          "integrations": [
                            "amazon_ads"
                          ],
                          "note": "Amazon Ads data cannot be queried through the API (Amazon Ads API terms). Everything else works as for any other integration: it is listed in /v1/integrations, its fields and connections are published, its widgets are covered by the widget DSL and its reports and bindings are identified as amazon_ads."
                        },
                        "date_presets": {
                          "dsl": [
                            "today",
                            "yesterday",
                            "lastweek",
                            "thisweek",
                            "lastmonth",
                            "thismonth",
                            "lastxdays",
                            "lastxweeks",
                            "lastxmonths",
                            "yeartolastmonth",
                            "yeartolastweek",
                            "custom"
                          ],
                          "v1_to_dsl": {
                            "today": "today",
                            "yesterday": "yesterday",
                            "lastweeksunsat": null,
                            "lastweekmonsun": null
                          },
                          "dsl_only": [
                            "thisweek",
                            "lastweek",
                            "yeartolastmonth",
                            "yeartolastweek"
                          ],
                          "uses_account_week_start": [
                            "lastweek",
                            "thisweek",
                            "lastxweeks",
                            "yeartolastweek"
                          ]
                        },
                        "composition": {
                          "max_sections": 12,
                          "max_widgets_per_section": 12,
                          "max_widgets_per_composition": 60,
                          "builder_generation_units": 3
                        },
                        "summary": {
                          "max_widgets_in_widget_scope": 2,
                          "max_widgets_in_section_scope": 15,
                          "max_sections_in_report_scope": 8,
                          "max_widgets_per_section_in_report_scope": 15
                        },
                        "ai_briefs": {
                          "levels": [
                            {
                              "level": "account",
                              "written_by_this_api": false,
                              "written_by": "the application",
                              "note": "The brief of the agency, which every summary of the account reads. It is an account setting and no call of this API writes it, like the default localization and the default brand template beside it."
                            },
                            {
                              "level": "report",
                              "written_by_this_api": true,
                              "field": "settings.ai_brief",
                              "written_by": "/reports/update",
                              "read_by": "/reports/get"
                            },
                            {
                              "level": "widget",
                              "written_by_this_api": true,
                              "field": "ai_summary.ai_brief",
                              "written_by": "/widgets/create, /widgets/update",
                              "read_by": "/widgets/get"
                            }
                          ],
                          "levels_note": "The three are ADDED UP, not chosen between: a summary of a widget reads the brief of the account, the one of its report and its own, in that order, and the nearest the widget is the one the model reads last. Two of them are written here and the account one is not.",
                          "max_length": 2000
                        },
                        "schedules": {
                          "frequencies": [
                            "daily",
                            "weekly",
                            "monthly"
                          ],
                          "frequencies_note": "A schedule listed with frequency 'none' exists but does not fire; it is not a frequency a schedule can be created with.",
                          "time": "on the hour, HH:00",
                          "timezone": "account"
                        },
                        "portal_views": {
                          "required": [
                            "name",
                            "report_keys",
                            "user_keys"
                          ],
                          "send_notification_requires_confirmation": true,
                          "lengths": {
                            "name": {
                              "min": 2,
                              "max": 150
                            },
                            "internal_notes": {
                              "min": 0,
                              "max": 250
                            }
                          },
                          "report_types_allowed": [
                            "report",
                            "template_based"
                          ]
                        },
                        "users": {
                          "create_role": "client",
                          "requires_confirmation": true,
                          "required": [
                            "email",
                            "first_name",
                            "last_name"
                          ],
                          "lengths": {
                            "email": {
                              "min": 0,
                              "max": 255
                            },
                            "first_name": {
                              "min": 2,
                              "max": 50
                            },
                            "last_name": {
                              "min": 2,
                              "max": 50
                            }
                          }
                        },
                        "versioning": {
                          "version_covers": "sections structure",
                          "if_version_supported": true,
                          "open_editor_wins_positions": true,
                          "if_version_unsupported_is_rejected": true,
                          "if_version_unsupported_note": "if_version is accepted only by the endpoints whose entry in endpoints[] says supports_if_version: true. Sending it to any other one is INVALID_VALUE: the write is not made while pretending the guard was checked.",
                          "if_version_compared_in_transaction": [
                            "/sections/create",
                            "/sections/copy",
                            "/sections/duplicate",
                            "/sections/reorder",
                            "/sections/delete"
                          ],
                          "if_version_best_effort": [
                            "/reports/append",
                            "/widgets/create",
                            "/widgets/create-image"
                          ],
                          "if_version_best_effort_note": "The routes of if_version_compared_in_transaction compare the number and write in one single step, so no other write to the report can land in between. The ones listed as best effort compare it before writing and then write something that does not change the list of sections at all -- a widget belongs to a section and the version counts sections -- so there it means 'refuse if the structure of the report moved since I read it', which is a coherence check and not an exclusive lock.",
                          "widget_hash_supported": true,
                          "widget_hash_covers": "the definition of a widget, its format, its custom header literals, its connection and its internal notes",
                          "widget_hash_excludes": "the row, the column, the section and the full width of the widget, which are its place and not its definition",
                          "widget_hash_note": "widgets/get publishes widget_hash; send it back as if_widget_hash on the endpoints whose entry in endpoints[] says supports_if_widget_hash: true. A stale one is WIDGET_CHANGED and details.stored_hash carries the current value, so the retry can reuse the same idempotency_key: if_widget_hash is not part of what that key identifies.",
                          "position_guard": true,
                          "position_guard_covers": "the section, the row and the column a widget is in",
                          "position_guard_note": "if_position is {section_key, row, column} as a read published them, and it is taken by the endpoints whose entry in endpoints[] says supports_if_position: true. A widget that is somewhere else is POSITION_CHANGED, with details.stored carrying where it really is; like if_widget_hash, it is outside what the idempotency_key identifies, so the retry reuses the same key. Every field of it is optional: what is sent is what is compared.",
                          "locks": {
                            "best_effort": true,
                            "scope": "one section",
                            "ttl_seconds": 30,
                            "retry_after_seconds": 2,
                            "taken_by": [
                              "/widgets/create",
                              "/widgets/update",
                              "/widgets/move",
                              "/widgets/copy"
                            ],
                            "not_taken_by_note": "The writes that touch the LIST of sections of a report --/sections/reorder and /sections/delete -- take no section lock: what they write is one column of the report, guarded by if_version inside the transaction that writes it.",
                            "error_code": "SECTION_BUSY",
                            "note": "A write that places a widget holds the section or sections it touches while it writes, so two callers do not renumber the same section at once. A caller that finds one busy is answered SECTION_BUSY with a Retry-After. It is best effort: the lock expires on its own and is not held across a transaction, so it coordinates callers and never guarantees exclusion."
                          }
                        },
                        "reports_get": {
                          "max_widgets": 500,
                          "note": "A report with more widgets than this comes back cut on a section boundary, with truncated: true."
                        },
                        "filters": {
                          "endpoints": [
                            "/filters/list",
                            "/filters/get"
                          ],
                          "conditions_shape": "widget_dsl.filters",
                          "conditions_shape_note": "The conditions of a saved filter are published in the same shape as the filters block of a widget definition: field, operator, value and type_of_field, under one combination."
                        },
                        "calculated_metrics": {
                          "endpoints": [
                            "/calculated-metrics/list",
                            "/calculated-metrics/get"
                          ],
                          "types": [
                            "expression",
                            "function",
                            "filtered"
                          ],
                          "types_note": "An expression carries a formula, a function carries a metric aggregated over a period, and a filtered one carries a metric read through a saved filter. Only the block of its own kind is filled in; the other two are null."
                        },
                        "brand_templates": {
                          "endpoints": [
                            "/brand-templates/list",
                            "/brand-templates/get"
                          ],
                          "system_key": "",
                          "system_key_note": "The brand of the platform is listed under the empty key, which is what a report stores when it prints with it and what /reports/update takes in brand_template.",
                          "cover_styles": [
                            "gradient",
                            "plain",
                            "aurora",
                            "circles",
                            "waves",
                            "diagonal",
                            "rings",
                            "dots",
                            "bubbles",
                            "contours",
                            "blocks",
                            "image"
                          ]
                        },
                        "language_files": {
                          "endpoints": [
                            "/language-files/list",
                            "/language-files/get"
                          ],
                          "system_count": 16,
                          "system_keys": [
                            "language_file_da",
                            "language_file_nl",
                            "language_file_en",
                            "language_file_fi",
                            "language_file_fr",
                            "language_file_de",
                            "language_file_el",
                            "language_file_it",
                            "language_file_no",
                            "language_file_pl",
                            "language_file_pt",
                            "language_file_ru",
                            "language_file_es",
                            "language_file_sv",
                            "language_file_tr",
                            "language_file_uk"
                          ],
                          "literals_published": false
                        },
                        "email_templates": {
                          "endpoints": [
                            "/email-templates/list",
                            "/email-templates/get"
                          ],
                          "formats": [
                            "plain_text",
                            "html"
                          ],
                          "body_published": false,
                          "body_note": "The body of a template is not published by a listing: it is up to seventy five thousand characters and the key, the subject and the sender are what /schedules/create needs. /email-templates/get publishes it, which is where a template is read before it is changed."
                        },
                        "snapshots": {
                          "endpoints": [
                            "/snapshots/list"
                          ],
                          "write_endpoints": [
                            "/reports/take-snapshot",
                            "/snapshots/delete"
                          ],
                          "types": [
                            "manual",
                            "scheduled"
                          ],
                          "statuses": [
                            "success",
                            "error"
                          ]
                        },
                        "reports": {
                          "update": {
                            "path": "/reports/update",
                            "accepted_fields": [
                              "name",
                              "internal_name",
                              "subtitle",
                              "internal_notes",
                              "share",
                              "data_sources",
                              "date_range",
                              "brand_template",
                              "front_cover",
                              "pdf",
                              "localization",
                              "language_file",
                              "substitution_variables",
                              "widgets_show",
                              "snapshots_with_scheduled_executions",
                              "ai_brief",
                              "brand_template_name",
                              "language_file_name"
                            ],
                            "partial": true
                          },
                          "set_connections": {
                            "path": "/reports/set-connections",
                            "atomic": false
                          },
                          "copy": {
                            "path": "/reports/copy"
                          },
                          "duplicate_for_client": {
                            "path": "/reports/duplicate-for-client",
                            "atomic": false
                          }
                        },
                        "editing": [
                          {
                            "key": "template_based_reports",
                            "statement": "In a report built on a template only the TEXT twin it keeps of its own is written: /widgets/update changes that text and the answer says affects_only_this_report. A widget inherited from the template, a twin holding a spreadsheet, and everything that is structure -- the sections, their order, the place of a widget, the pictures -- belong to the template and are written there, which moves every report built on it.",
                            "refused_with": {
                              "error_code": "APPEND_NOT_ALLOWED",
                              "reason": "template_based"
                            }
                          },
                          {
                            "key": "full_width_when_a_widget_is_updated",
                            "statement": "full_width asks for a row of its own and the place of a widget belongs to the report, so /widgets/update reads every other key of the definition and leaves the widget exactly where it is; move it with /widgets/move. The editor has no control for the flag either: it is written by a definition and cleared by the renumbering of a row that ends up shared."
                          },
                          {
                            "key": "the_level_at_which_a_report_binds_its_data_sources",
                            "statement": "No call of this API writes report_level_data_sources, now or later. Moving a report to per widget bindings deletes its binding rows and the way back is rebinding every widget by hand: it is done in the Data sources window of the application and nowhere else. Which level a report is at is published by /reports/get, and it decides which of the two rebinding routes applies.",
                            "refused_with": {
                              "error_code": "UNKNOWN_PARAM"
                            }
                          }
                        ],
                        "guarantees": {
                          "confirmation_token_ttl_seconds": 600,
                          "idempotency_ttl_seconds": 86400,
                          "idempotency_reserve_ttl_seconds": 120,
                          "idempotency_key_max_length": 128,
                          "trash": {
                            "retention_days": 7,
                            "list": "/reports/trash",
                            "restore": "/reports/restore",
                            "permanent_delete": "/reports/delete with permanent=true",
                            "permanent_delete_is_final": true,
                            "kept": [
                              "widgets",
                              "data sources",
                              "client briefs",
                              "share link settings"
                            ],
                            "not_restored": [
                              "schedules"
                            ],
                            "counts_as_in_use": true
                          }
                        },
                        "endpoints": [
                          {
                            "path": "/reports/create",
                            "requires_confirmation": false,
                            "confirmation_mode": "never",
                            "supports_if_version": false
                          },
                          {
                            "path": "/reports/create-from-template",
                            "requires_confirmation": false,
                            "confirmation_mode": "never",
                            "supports_if_version": false
                          },
                          {
                            "path": "/reports/restore",
                            "requires_confirmation": false,
                            "confirmation_mode": "never",
                            "supports_if_version": false
                          }
                        ],
                        "endpoints_note": "endpoints[] describes the calls that CHANGE something: what each one confirms, what it audits and what undoes it. The reads are listed by the block that describes them.",
                        "account": {
                          "plan_reports": 40,
                          "plan_templates": 30,
                          "reports_used": 12,
                          "templates_used": 3,
                          "public_api_plan_accounts": 30,
                          "write_access_enabled": true
                        }
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/reports/create": {
      "post": {
        "operationId": "createReport",
        "tags": [
          "Reports"
        ],
        "summary": "Create a report from a composition",
        "description": "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.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the create_report tool to turn a conversation into a report. Discover connections, fields and capabilities first; it writes on the first try, without asking for a confirmation, and delete_report undoes it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportsCreateRequest"
              },
              "examples": {
                "catalog_and_custom": {
                  "summary": "One section copied from the catalog and one written from scratch.",
                  "value": {
                    "name": "Meta Ads -- monthly",
                    "language": "en",
                    "date_range": {
                      "preset": "lastmonth"
                    },
                    "data_sources": [
                      {
                        "integration_id": "google_ads",
                        "connection_key": "6e2a90c3b15d47f8a0d9c46e81375b2a",
                        "account_id": "574-118-2096"
                      },
                      {
                        "integration_id": "facebook_ads",
                        "connection_key": "3d5e8a1c72b04f96ad3c1e70b542986f",
                        "account_id": "act_412298370015"
                      }
                    ],
                    "sections": [
                      {
                        "type": "catalog",
                        "slug": "cross-platform-ads",
                        "position": 0,
                        "add_summary": null,
                        "show_previous_period": true
                      },
                      {
                        "type": "custom",
                        "name": "Performance",
                        "add_summary": {
                          "position": "last"
                        },
                        "show_previous_period": null,
                        "widgets": [
                          {
                            "integration_id": "facebook_ads",
                            "dsl": {
                              "name": "Spend by campaign",
                              "visualization": "table_dimensions_metrics",
                              "chart_type": null,
                              "full_width": false,
                              "content": null,
                              "data_view": "campaigns",
                              "metrics": [
                                "spend",
                                "impressions",
                                "clicks"
                              ],
                              "dimensions": [
                                "campaign_name"
                              ],
                              "filter_key": null,
                              "compare": null,
                              "filters": null,
                              "sort_by": {
                                "field": "spend",
                                "direction": "desc"
                              },
                              "limit": 10,
                              "date_range": null,
                              "previous_period": null,
                              "show_change": null,
                              "settings": null,
                              "unfulfilled": []
                            }
                          }
                        ]
                      }
                    ],
                    "idempotency_key": "report-meta-monthly-2026-09"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportsCreateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The report exists; created_keys lists everything the call wrote.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "app_url": "https://app.reportingninja.com/#!report/9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "type": "report",
                        "name": "Meta Ads -- monthly",
                        "version": 1,
                        "created_via": "api",
                        "sections": [
                          {
                            "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                            "name": "Overview",
                            "origin": "catalog",
                            "widgets": [
                              {
                                "widget_key": "b8e1470d29a54c36ae0f7b2914c8d365",
                                "name": "Cost and conversions",
                                "row": 1,
                                "column": 1,
                                "integration_id": "google_ads"
                              }
                            ]
                          },
                          {
                            "section_key": "e13f97b25a684cd0b7e4a8236f95d108",
                            "name": "Performance",
                            "origin": "custom",
                            "widgets": [
                              {
                                "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                                "name": "Spend by campaign",
                                "row": 1,
                                "column": 1,
                                "integration_id": "facebook_ads"
                              }
                            ]
                          }
                        ],
                        "created_keys": [
                          "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                          "c6a04e83d71b429fa58d2c9e37016b52",
                          "b8e1470d29a54c36ae0f7b2914c8d365",
                          "e13f97b25a684cd0b7e4a8236f95d108",
                          "7a5d2c910eb34f68b9c07d4e15382fa6"
                        ],
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run": {
                    "summary": "The same body with dry_run: true. Nothing is written; would_create says what it would do.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "dry_run": true,
                        "valid": true,
                        "type": "report",
                        "name": "Meta Ads -- monthly",
                        "language": "en",
                        "would_create": {
                          "sections": 2,
                          "widgets": 1,
                          "section_summaries": 1
                        },
                        "sections": [
                          {
                            "name": "Overview",
                            "origin": "catalog",
                            "slug": "cross-platform-ads",
                            "position": 0,
                            "catalog_version": 1,
                            "integrations": [
                              {
                                "integration_id": "google_ads"
                              }
                            ],
                            "add_summary": null,
                            "show_previous_period": true
                          },
                          {
                            "name": "Performance",
                            "origin": "custom",
                            "integrations": [
                              {
                                "integration_id": "facebook_ads"
                              }
                            ],
                            "add_summary": {
                              "position": "last"
                            },
                            "show_previous_period": false,
                            "widgets": [
                              {
                                "widget_key": null,
                                "name": "Spend by campaign",
                                "row": 1,
                                "column": 1,
                                "integration_id": "facebook_ads"
                              }
                            ]
                          }
                        ],
                        "add_summary": false,
                        "data_sources": 2,
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run_invalid": {
                    "$ref": "#/components/examples/WriteDryRunInvalid"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/reports/create-from-template": {
      "post": {
        "operationId": "createReportFromTemplate",
        "tags": [
          "Reports"
        ],
        "summary": "Create a report from a catalog template",
        "description": "Materializes a published catalog entry into a report, or into one of the account's own templates, and binds it to the connections given in data_sources. With section_positions only the named sections are written. A parameterized entry can only be materialized as a report and asks for its parameter_values. This endpoint never asks for a confirmation and does not accept if_version. Refusals: 400 VALIDATION_FAILED, UNKNOWN_PARAM or MISSING_REQUIRED_FIELD, 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 for an entry, version or position that is not published and NOT_FOUND for a language file the account does not own, 409 for the idempotency conflicts. This is the endpoint that binds a whole entry at once, so it is the one that can answer the warning DATA_SOURCES_PARTIALLY_BOUND: the report is created, and the sections whose integration has no data source are named in warnings[].\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the create_report_from_template tool when list_template_catalog already has what the user asked for: it is cheaper and more complete than composing the same report by hand.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportsCreateFromTemplateRequest"
              },
              "examples": {
                "whole_entry": {
                  "summary": "The whole entry, bound to one connection.",
                  "value": {
                    "catalog_slug": "cross-platform-ads",
                    "name": "Acme -- PPC",
                    "data_sources": [
                      {
                        "integration_id": "google_ads",
                        "connection_key": "6e2a90c3b15d47f8a0d9c46e81375b2a",
                        "account_id": "574-118-2096"
                      },
                      {
                        "integration_id": "facebook_ads",
                        "connection_key": "3d5e8a1c72b04f96ad3c1e70b542986f",
                        "account_id": "act_412298370015"
                      }
                    ],
                    "idempotency_key": "acme-ppc-from-catalog-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportsCreateFromTemplateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The report exists with the sections of the entry.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "report_key": "4d81b0e6c5a74f3298ad6e2b9c105f37",
                        "app_url": "https://app.reportingninja.com/#!report/4d81b0e6c5a74f3298ad6e2b9c105f37",
                        "type": "report",
                        "name": "Acme -- PPC",
                        "internal_name": "Cross-platform ads report",
                        "version": 1,
                        "created_via": "api",
                        "sections": [
                          {
                            "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                            "name": "Overview",
                            "origin": "catalog",
                            "widgets": [
                              {
                                "widget_key": "b8e1470d29a54c36ae0f7b2914c8d365",
                                "name": "Cost and conversions",
                                "row": 1,
                                "column": 1,
                                "integration_id": "google_ads"
                              }
                            ]
                          },
                          {
                            "section_key": "e13f97b25a684cd0b7e4a8236f95d108",
                            "name": "Meta Ads",
                            "origin": "catalog",
                            "widgets": [
                              {
                                "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                                "name": "Spend by campaign",
                                "row": 1,
                                "column": 1,
                                "integration_id": "facebook_ads"
                              }
                            ]
                          }
                        ],
                        "created_keys": [
                          "4d81b0e6c5a74f3298ad6e2b9c105f37",
                          "c6a04e83d71b429fa58d2c9e37016b52",
                          "b8e1470d29a54c36ae0f7b2914c8d365",
                          "e13f97b25a684cd0b7e4a8236f95d108",
                          "7a5d2c910eb34f68b9c07d4e15382fa6"
                        ],
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run": {
                    "summary": "The same body with dry_run: true.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "dry_run": true,
                        "valid": true,
                        "catalog": {
                          "slug": "cross-platform-ads",
                          "version": 1
                        },
                        "type": "report",
                        "name": "Acme -- PPC",
                        "internal_name": "Cross-platform ads report",
                        "language": "en",
                        "would_create": {
                          "sections": 2
                        },
                        "sections": [
                          {
                            "position": 0,
                            "name": "Overview",
                            "origin": "catalog",
                            "integrations": [
                              {
                                "integration_id": "google_ads"
                              }
                            ]
                          },
                          {
                            "position": 1,
                            "name": "Meta Ads",
                            "origin": "catalog",
                            "integrations": [
                              {
                                "integration_id": "facebook_ads"
                              }
                            ]
                          }
                        ],
                        "data_sources": 2,
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run_invalid": {
                    "$ref": "#/components/examples/WriteDryRunInvalid"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/reports/append": {
      "post": {
        "operationId": "appendSections",
        "tags": [
          "Reports"
        ],
        "summary": "Append sections to an existing report",
        "description": "Adds sections to a report that already exists, with the same composition vocabulary /v1/reports/create takes. 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. 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`. A section of an append carries `add_summary` and `show_previous_period` like a section of a creation, and the summary OF A SECTION is accepted here -- what an append does not write is the summary of the whole report. It spends AI credits of the account when the report is opened, viewed or delivered by a schedule, never when it is written. Two kinds of report cannot receive sections and answer 400 APPEND_NOT_ALLOWED with details.reason: a template_based report, whose sections are its template's, and a report whose widgets each carry their own connection. That second one takes an append when every widget of the body that reads data brings its connection_key and account_id; a widget whose dsl compares connected accounts brings them in compare.items instead and needs no pair of its own. This endpoint accepts if_version. Refusals: 400 VALIDATION_FAILED, UNKNOWN_PARAM or APPEND_NOT_ALLOWED, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND for the report and TEMPLATE_NOT_FOUND for a catalog section that is no longer published, 409 VERSION_MISMATCH with details {expected, actual} plus the idempotency conflicts. The 403 of this endpoint is only ever WRITE_ACCESS_DISABLED: adding sections to a report that already exists does not spend a unit of the plan, so PLAN_LIMIT_REACHED belongs to the two endpoints that create a report. This endpoint never asks for a confirmation, so it never answers CONFIRMATION_EXPIRED or CONFIRMATION_MISMATCH. The undo of an append is deleting the sections it created.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the append_sections tool to grow a report the conversation already produced, with the same vocabulary create_report takes. Read the report's version with get_report and send it as if_version if anyone else may be editing it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportsAppendRequest"
              },
              "examples": {
                "one_custom_section": {
                  "summary": "One section written from scratch, guarded with the version the report was read at.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "if_version": 7,
                    "sections": [
                      {
                        "type": "custom",
                        "name": "Creatives",
                        "add_summary": null,
                        "show_previous_period": null,
                        "widgets": [
                          {
                            "integration_id": "facebook_ads",
                            "dsl": {
                              "name": "Spend by campaign",
                              "visualization": "table_dimensions_metrics",
                              "chart_type": null,
                              "full_width": false,
                              "content": null,
                              "data_view": "campaigns",
                              "metrics": [
                                "spend",
                                "impressions",
                                "clicks"
                              ],
                              "dimensions": [
                                "campaign_name"
                              ],
                              "filter_key": null,
                              "compare": null,
                              "filters": null,
                              "sort_by": {
                                "field": "spend",
                                "direction": "desc"
                              },
                              "limit": 10,
                              "date_range": null,
                              "previous_period": null,
                              "show_change": null,
                              "settings": null,
                              "unfulfilled": []
                            }
                          }
                        ]
                      }
                    ],
                    "idempotency_key": "meta-monthly-append-creatives-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportsAppendResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The section was added and the report moved to version 8.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "version": 8,
                        "sections": [
                          {
                            "section_key": "e13f97b25a684cd0b7e4a8236f95d108",
                            "name": "Creatives",
                            "origin": "custom",
                            "widgets": [
                              {
                                "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                                "name": "Spend by campaign",
                                "row": 1,
                                "column": 1,
                                "integration_id": "facebook_ads"
                              }
                            ]
                          }
                        ],
                        "created_keys": [
                          "e13f97b25a684cd0b7e4a8236f95d108",
                          "7a5d2c910eb34f68b9c07d4e15382fa6"
                        ],
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run": {
                    "summary": "The same body with dry_run: true.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "dry_run": true,
                        "valid": true,
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "version": 7,
                        "would_create": {
                          "sections": 1,
                          "widgets": 1,
                          "section_summaries": 0
                        },
                        "sections": [
                          {
                            "name": "Creatives",
                            "origin": "custom",
                            "integrations": [
                              {
                                "integration_id": "facebook_ads"
                              }
                            ],
                            "add_summary": null,
                            "show_previous_period": false,
                            "widgets": [
                              {
                                "widget_key": null,
                                "name": "Spend by campaign",
                                "row": 1,
                                "column": 1,
                                "integration_id": "facebook_ads"
                              }
                            ]
                          }
                        ],
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run_invalid": {
                    "$ref": "#/components/examples/WriteDryRunInvalid"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/reports/delete": {
      "post": {
        "operationId": "deleteReport",
        "tags": [
          "Reports"
        ],
        "summary": "Delete a report",
        "description": "Deletes a report. By default it goes to the trash: it leaves every listing and every plan count, its key stops resolving and its share link stops working, and /v1/reports/restore brings it back with its original type for 7 days. /v1/reports/trash lists what is in there. Sending permanent: true deletes the report for good instead, with its sections and its widgets, and that cannot be undone. The scheduled deliveries of the report are deleted either way and a restore does not recreate them. This endpoint ALWAYS asks for a confirmation, in both forms: the first call answers HTTP 200 with confirmation_required, a token and a preview counting what would go; the same body sent again with that token performs the deletion. A report that a portal view or a template_based report still depends on is refused with 409 REPORT_IN_USE and details naming them, so they can be detached first; an entry of that list that is itself in the trash says so with in_trash. A report already in the trash is refused with 409 REPORT_IN_TRASH unless the call carries permanent: true. A report whose scheduled delivery is being sent right now is refused with 409 SCHEDULE_QUEUED and is worth retrying in a few minutes. A retry with the same idempotency_key answers the original call even if the report changed state in between; read /v1/reports/get for its current state. Refusals: 400 for the framework and the body, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 REPORT_IN_USE, REPORT_IN_TRASH or SCHEDULE_QUEUED plus the confirmation and idempotency conflicts. if_version is not accepted: there is no version left to guard.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the delete_report tool to undo a create_report. It always confirms first: show the user what the preview says will go before you send the token. Without permanent the report goes to a 7-day trash that restore_report undoes and list_trash shows; with permanent it is gone for good.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportsDeleteRequest"
              },
              "examples": {
                "confirm_then_delete": {
                  "summary": "The first call carries no token and answers one; the second call is this same body with confirmation_token added.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "idempotency_key": "delete-meta-monthly-1"
                  }
                },
                "permanent": {
                  "summary": "Skip the trash. The same two calls, and the token is bound to this body: one issued without permanent does not apply this one.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "permanent": true,
                    "idempotency_key": "delete-meta-monthly-permanent-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportsDeleteResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The second call, with the token. The report is in the trash.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "deleted": true,
                        "permanent": false,
                        "trashed_at": "2026-09-18T10:30:00Z",
                        "restorable_until": "2026-09-25T10:30:00Z",
                        "restore": "/reports/restore",
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "ok_permanent": {
                    "summary": "The same call with permanent: true. The report is gone for good.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "deleted": true,
                        "permanent": true,
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run": {
                    "summary": "dry_run: true. Nothing is deleted and the counts say what would go.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "dry_run": true,
                        "valid": true,
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "name": "Meta Ads -- monthly",
                        "type": "report",
                        "version": 7,
                        "sections": 3,
                        "widgets": 14,
                        "will_delete_schedules": 1,
                        "permanent": false,
                        "retention_days": 7,
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run_invalid": {
                    "$ref": "#/components/examples/WriteDryRunInvalid"
                  },
                  "confirmation_required": {
                    "$ref": "#/components/examples/WriteConfirmationRequired"
                  },
                  "replayed": {
                    "$ref": "#/components/examples/WriteReplayed"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/reports/restore": {
      "post": {
        "operationId": "restoreReport",
        "tags": [
          "Reports"
        ],
        "summary": "Restore a report from the trash",
        "description": "Takes a report out of the trash and gives it back the type it had before /v1/reports/delete put it there, so it shows up again in its listing and counts against the plan again. /v1/reports/trash lists what can be restored and until when. Because a report in the trash does not count against the plan, the account may have filled that room since: a restore that does not fit is refused with 403 PLAN_LIMIT_REACHED. A report that is not in the trash is refused with 409 REPORT_NOT_IN_TRASH. The scheduled deliveries the report had were deleted when it went to the trash and are NOT recreated: create them again with /v1/schedules/create. This endpoint never asks for a confirmation: it only adds a report back. A retry with the same idempotency_key answers the original call even if the report changed state in between; read /v1/reports/get for its current state. Refusals: 400 for the framework and the body, 403 WRITE_ACCESS_DISABLED or PLAN_LIMIT_REACHED, 404 NOT_FOUND, 409 REPORT_NOT_IN_TRASH plus the idempotency conflicts. if_version is not accepted: a restore does not change the structure of the report.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the restore_report tool to undo a delete_report that did not carry permanent, while the report is still in the trash. It needs no confirmation, and it can be refused with PLAN_LIMIT_REACHED because the report starts counting against the plan again.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportsRestoreRequest"
              },
              "examples": {
                "restore": {
                  "summary": "The report goes back to the account with its original type.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "idempotency_key": "restore-meta-monthly-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportsRestoreResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The report is back, as the report it was.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "app_url": "https://app.reportingninja.com/#!report/9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "restored": true,
                        "type": "report",
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run": {
                    "summary": "dry_run: true. Nothing is restored and the preview says what would come back.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "dry_run": true,
                        "valid": true,
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "name": "Meta Ads -- monthly",
                        "type": "report",
                        "version": 7,
                        "sections": 3,
                        "widgets": 14,
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run_invalid": {
                    "$ref": "#/components/examples/WriteDryRunInvalid"
                  },
                  "replayed": {
                    "summary": "A retry with the same idempotency_key and the same body. The stored answer comes back with the meta of THIS request and the id of the call that did the work -- even if the report went back to the trash in between.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "app_url": "https://app.reportingninja.com/#!report/9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "restored": true,
                        "type": "report",
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_9z8y7x6w5v4u",
                        "timestamp": "2026-09-18T10:31:12Z",
                        "replayed_from_request_id": "req_a1b2c3d4e5f6",
                        "replayed": true
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/widgets/create": {
      "post": {
        "operationId": "createWidget",
        "tags": [
          "Widgets"
        ],
        "summary": "Add a widget to a section",
        "description": "Writes one widget into a section of a report. The widget is defined in the widget DSL, the same vocabulary /v1/reports/get answers for the widgets it can express. 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`. The integration must already have a data source in the report, and integration_id may be omitted only for a dsl whose `visualization` is `text`: a text block is prose and reads no data. A text block written into a TEMPLATE is cloned into the reports based on it, like any other widget of a template. A section holds at most 12 widgets. Without placement the widget is placed automatically. This endpoint accepts if_version and verify_data, and never asks for a confirmation, so it never answers CONFIRMATION_EXPIRED or CONFIRMATION_MISMATCH. With verify_data in a dry run the widget IS run against the provider and the verdict travels in data.verification, but the answer is still valid: true -- a rehearsal reports what it found and refuses nothing; the 422 belongs to the real write. Two callers writing into the same section at once are serialised: the second one gets 409 SECTION_BUSY with a Retry-After. An AI summary is written with ai_summary in place of the dsl, and the two are mutually exclusive: it is refused on a TEMPLATE (APPEND_NOT_ALLOWED reason template_report), it never carries a connection, and an empty widget_keys or section_keys is refused -- leave the key out to have the platform choose what it reads.\n\nRefusals: 400 VALIDATION_FAILED (including the problem codes DSL_INVALID and TRANSLATION_FAILED), UNKNOWN_PARAM, APPEND_NOT_ALLOWED or INVALID_VALUE for a placement that does not fit, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 VERSION_MISMATCH or SECTION_BUSY plus the idempotency conflicts, 422 WIDGET_EXECUTION_FAILED when verify_data ran the widget and the provider refused it. Creating a widget does not move the version of the report: that number covers the list of sections.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.\n\nA COMPARATOR is written with the `compare` block of the dsl: `by` accounts draws one series per connected account and names at least two of them in `items`, so such a widget carries no `connection_key` of its own -- which is also how it is written into a report that binds a connection per widget, where the items ARE the connections of the widget and a body sending the pair as well is refused with 400 INVALID_VALUE naming connection_key; `by` filters draws one per saved filter of the account, reads one connection like any ordinary widget and names it in the body; `by` breakdown splits one page metric of facebook_insights into a series per value. Only a graph_basic, a graph_pie, a graph_trend and a table_metrics_summary take one, and which classes and which axes an integration offers depends on the entity level of the data view as well as on the integration. The same account or the same saved filter named twice is refused, and a `label` cannot carry the sequence #%#, which is what the parts of a stored account are joined with. A comparator of connected accounts is not written into a report whose report_level_data_sources is true, which answers 409 NOT_ALLOWED with details.reason report_level_data_sources.",
        "x-mcp-description": "Use the create_widget tool to add one chart, table or KPI to a report that exists. Take the field ids from list_fields and the data_view of the dsl from the dsl_data_views of get_capabilities, which is not the data view list of describe_integration; never guess either. verify_data runs the widget against the provider before writing it and can take up to 45 seconds.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WidgetsCreateRequest"
              },
              "examples": {
                "automatic_placement": {
                  "summary": "Placed automatically at the end of the section.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                    "integration_id": "facebook_ads",
                    "dsl": {
                      "name": "Spend by campaign",
                      "visualization": "table_dimensions_metrics",
                      "chart_type": null,
                      "full_width": false,
                      "content": null,
                      "data_view": "campaigns",
                      "metrics": [
                        "spend",
                        "impressions",
                        "clicks"
                      ],
                      "dimensions": [
                        "campaign_name"
                      ],
                      "filter_key": null,
                      "compare": null,
                      "filters": null,
                      "sort_by": {
                        "field": "spend",
                        "direction": "desc"
                      },
                      "limit": 10,
                      "date_range": null,
                      "previous_period": null,
                      "show_change": null,
                      "settings": null,
                      "unfulfilled": []
                    },
                    "idempotency_key": "meta-monthly-spend-by-campaign-1"
                  }
                },
                "with_verify_data": {
                  "summary": "The same widget, run against the provider before it is written. It can take up to 45 seconds, and a failure verdict refuses the write with 422.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                    "integration_id": "facebook_ads",
                    "dsl": {
                      "name": "Spend by campaign",
                      "visualization": "table_dimensions_metrics",
                      "chart_type": null,
                      "full_width": false,
                      "content": null,
                      "data_view": "campaigns",
                      "metrics": [
                        "spend",
                        "impressions",
                        "clicks"
                      ],
                      "dimensions": [
                        "campaign_name"
                      ],
                      "filter_key": null,
                      "compare": null,
                      "filters": null,
                      "sort_by": {
                        "field": "spend",
                        "direction": "desc"
                      },
                      "limit": 10,
                      "date_range": null,
                      "previous_period": null,
                      "show_change": null,
                      "settings": null,
                      "unfulfilled": []
                    },
                    "placement": {
                      "row": 2,
                      "column": 1
                    },
                    "verify_data": true,
                    "idempotency_key": "meta-monthly-spend-by-campaign-verified-1"
                  }
                },
                "text_block": {
                  "summary": "A block of prose. It reads no data, so it carries no integration_id and the empty value of every data key, and it takes a row of its own.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                    "dsl": {
                      "name": "How to read this section",
                      "visualization": "text",
                      "chart_type": null,
                      "full_width": true,
                      "content": "This section covers **paid media only**. Organic traffic is reported separately.\n\n- Spend is the amount invoiced by the platform.\n- Conversions use the attribution window configured in each account.",
                      "data_view": "",
                      "metrics": [],
                      "dimensions": [],
                      "filter_key": null,
                      "compare": null,
                      "filters": null,
                      "sort_by": null,
                      "limit": null,
                      "date_range": null,
                      "previous_period": null,
                      "show_change": null,
                      "settings": null,
                      "unfulfilled": []
                    },
                    "idempotency_key": "meta-monthly-how-to-read-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WidgetsCreateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The widget exists, placed in the first free slot.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                        "name": "Spend by campaign",
                        "integration_id": "facebook_ads",
                        "row": 2,
                        "column": 1,
                        "placement_adjusted": false,
                        "unfulfilled": [],
                        "warnings": [],
                        "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                        "created_keys": [
                          "7a5d2c910eb34f68b9c07d4e15382fa6"
                        ]
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run": {
                    "summary": "The same body with dry_run: true.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "dry_run": true,
                        "valid": true,
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                        "name": "Spend by campaign",
                        "integration_id": "facebook_ads",
                        "row": 2,
                        "column": 1,
                        "placement_adjusted": false,
                        "unfulfilled": [],
                        "warnings": [],
                        "would_create": 1
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run_invalid": {
                    "$ref": "#/components/examples/WriteDryRunInvalid"
                  },
                  "verified": {
                    "summary": "verify_data: true in a DRY RUN. The widget was run against the provider and it refused it, and the answer is still valid: true -- nothing was going to be written anyway. Sent without dry_run, this same verdict is a 422 WIDGET_EXECUTION_FAILED and the widget is not created.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "dry_run": true,
                        "valid": true,
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                        "name": "Spend by campaign",
                        "integration_id": "facebook_ads",
                        "row": 2,
                        "column": 1,
                        "placement_adjusted": false,
                        "unfulfilled": [],
                        "warnings": [],
                        "verification": {
                          "verdict": "failure",
                          "message": "The metric 'cost_per_conversion' is not available for this data view",
                          "reason": null
                        },
                        "would_create": 1
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/WidgetExecutionFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/widgets/delete": {
      "post": {
        "operationId": "deleteWidget",
        "tags": [
          "Widgets"
        ],
        "summary": "Delete a widget",
        "description": "Deletes one widget of a section. There is no undo. This endpoint ALWAYS asks for a confirmation: the first call answers HTTP 200 with confirmation_required, a token and a preview of the widget, including parallel_copies -- how many copies of this widget live in the reports built on this template, all of which are DELETED with it. That number is what the confirmation is for: deleting one widget of a template removes it from every report that follows the template. if_version is not accepted: deleting a widget does not change the list of sections the version covers. if_widget_hash IS accepted, and refuses the deletion with 409 WIDGET_CHANGED when the widget was rewritten between the call that read it and this one. Refusals: 400 for the framework and the body, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND for the report, the section or the widget, 409 for the confirmation and idempotency conflicts.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the delete_widget tool to undo a create_widget. It always confirms first, and the preview counts the copies of the widget living in template based reports, which are deleted with it: show that number to the user before you send the token.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WidgetsDeleteRequest"
              },
              "examples": {
                "confirm_then_delete": {
                  "summary": "The first call carries no token and answers one; the second is this same body with confirmation_token added.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                    "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                    "idempotency_key": "delete-spend-by-campaign-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WidgetsDeleteResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The second call, with the token.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                        "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "deleted": true,
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run": {
                    "summary": "dry_run: true. Nothing is deleted.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "dry_run": true,
                        "valid": true,
                        "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                        "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "name": "Spend by campaign",
                        "row": 2,
                        "column": 1,
                        "integration_id": "facebook_ads",
                        "parallel_copies": 0,
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run_invalid": {
                    "$ref": "#/components/examples/WriteDryRunInvalid"
                  },
                  "confirmation_required": {
                    "$ref": "#/components/examples/WriteConfirmationRequired"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/widgets/update": {
      "post": {
        "operationId": "updateWidget",
        "tags": [
          "Widgets"
        ],
        "summary": "Rewrite the definition of a widget",
        "description": "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.\n\nWhat 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.\n\nWhat 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.\n\nEvery 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.\n\nRefusals: 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.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.\n\nWhat 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.",
        "x-mcp-description": "Use the update_widget tool to fix a widget that already exists instead of deleting it and creating another one: it keeps the connection, the format and the column titles a fresh widget would lose. Call get_widget first, edit the dsl it answered and send the widget_hash back as if_widget_hash. The answer carries a backup_key restore_widget puts back.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WidgetsUpdateRequest"
              },
              "examples": {
                "guarded": {
                  "summary": "The dsl of /v1/widgets/get with one metric added, guarded by the hash it answered.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                    "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                    "dsl": {
                      "name": "Spend and revenue by campaign",
                      "visualization": "table_dimensions_metrics",
                      "chart_type": null,
                      "full_width": null,
                      "content": null,
                      "data_view": "campaign",
                      "metrics": [
                        "spend",
                        "purchase_value"
                      ],
                      "dimensions": [
                        "campaign_name"
                      ],
                      "filter_key": null,
                      "compare": null,
                      "filters": null,
                      "sort_by": null,
                      "limit": null,
                      "date_range": null,
                      "previous_period": null,
                      "show_change": null,
                      "settings": null,
                      "unfulfilled": []
                    },
                    "if_widget_hash": "4c1f0b6e9a7d2358c04e1b9f7a3d6521e8b04c7f19a2d3586e0f4b17c92da638",
                    "idempotency_key": "add-revenue-column-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WidgetsUpdateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The widget as it was left, with the backup that undoes the call.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                        "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                        "widget": {
                          "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                          "name": "Spend and revenue by campaign",
                          "row": 2,
                          "column": 1,
                          "type": "table",
                          "integration_id": "facebook_ads",
                          "data_view": "campaign",
                          "editable": true
                        },
                        "widget_hash": "91ab3c70d5e24f68b0c7d1e953286fa4c60b1d87e35f902a4c718bd6e0f3a259",
                        "backup_key": "e7c3901a5db24f86b1c0d47e29358fa6",
                        "lost_properties": [],
                        "affects_only_this_report": false,
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-20T10:30:00Z"
                      }
                    }
                  },
                  "dry_run": {
                    "summary": "dry_run: true. Nothing is written and no backup is taken.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "dry_run": true,
                        "valid": true,
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                        "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                        "widget": {
                          "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                          "name": "Spend and revenue by campaign",
                          "row": 2,
                          "column": 1,
                          "type": "table",
                          "integration_id": "facebook_ads",
                          "data_view": "campaign",
                          "editable": true
                        },
                        "widget_hash": "91ab3c70d5e24f68b0c7d1e953286fa4c60b1d87e35f902a4c718bd6e0f3a259",
                        "backup_key": null,
                        "lost_properties": [],
                        "affects_only_this_report": false,
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-20T10:30:00Z"
                      }
                    }
                  },
                  "dry_run_invalid": {
                    "$ref": "#/components/examples/WriteDryRunInvalid"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/widgets/restore": {
      "post": {
        "operationId": "restoreWidget",
        "tags": [
          "Widgets"
        ],
        "summary": "Put a widget back from a backup",
        "description": "Puts the definition and the format a backup holds back onto the widget it was taken from. Every /v1/widgets/update and every /v1/widgets/restore keeps the widget as it was first, for 7 days, and /v1/widgets/get lists what is there under backup_keys.\n\nThe place and the connection are the CURRENT ones and not the backup's: a widget that was moved stays where it is and one that was rebound keeps reading what it reads. What comes back is what it showed and how it looked. The call takes a backup of its own before it writes, so a restore is undoable in the same way.\n\nRefusals: 400 for the framework and the body, 400 APPEND_NOT_ALLOWED with reason template_based, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND for the report, the section, the widget or a backup that is not this widget's, 409 WIDGET_NOT_EDITABLE, 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 ROW_RULE and 409 REPORT_IN_TRASH.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the restore_widget tool to undo an update_widget: pass the backup_key it answered. get_widget lists the backups of a widget when you no longer have it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WidgetsRestoreRequest"
              },
              "examples": {
                "undo": {
                  "summary": "Undoing the update above.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                    "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                    "backup_key": "e7c3901a5db24f86b1c0d47e29358fa6",
                    "idempotency_key": "undo-add-revenue-column-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WidgetsRestoreResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The widget as the backup held it, with a backup of what it was a moment ago.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                        "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                        "widget": {
                          "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                          "name": "Spend by campaign",
                          "row": 2,
                          "column": 1,
                          "type": "table",
                          "integration_id": "facebook_ads",
                          "data_view": "campaign",
                          "editable": true
                        },
                        "widget_hash": "4c1f0b6e9a7d2358c04e1b9f7a3d6521e8b04c7f19a2d3586e0f4b17c92da638",
                        "backup_key": "2f9a48c01e7d4b56a93c0f81d647be23",
                        "lost_properties": [],
                        "affects_only_this_report": false,
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-20T10:30:00Z"
                      }
                    }
                  },
                  "dry_run_invalid": {
                    "$ref": "#/components/examples/WriteDryRunInvalid"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/widgets/rename": {
      "post": {
        "operationId": "renameWidget",
        "tags": [
          "Widgets"
        ],
        "summary": "Rename a widget",
        "description": "Changes the title of one widget and nothing else. Every kind of widget has one -- a KPI, a chart, a table, a text block, an image, a page break and an AI summary all carry a name, and every read publishes it -- although the application only shows a name field on the ones that read data.\n\nIt takes no backup: the inverse of a rename is another rename with the name the read published. Nothing the widget shows is touched and no cached result is dropped.\n\nThe name is between 1 and 80 characters, the range the application uses; longer names already stored are left alone.\n\nRefusals: 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 renamed -- 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 WIDGET_CHANGED when if_widget_hash is stale, and 409 REPORT_IN_TRASH.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the rename_widget tool to change the title of a widget, whatever kind of widget it is. Use update_widget when what changes is what the widget shows.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WidgetsRenameRequest"
              },
              "examples": {
                "named": {
                  "summary": "The new title of one widget.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                    "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                    "name": "Spend and revenue by campaign",
                    "idempotency_key": "6f1c0b8e-2d54-4a71-9c3e-7b8a0d5f1e24"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WidgetsRenameResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/widgets/set-connection": {
      "post": {
        "operationId": "setWidgetConnection",
        "tags": [
          "Widgets"
        ],
        "summary": "Point a widget at another connection",
        "description": "Changes which connection and which account ONE widget reads through, in the reports that bind their data sources per widget -- the ones whose report_level_data_sources is false. A report that binds once for the whole report has no per widget connection to set and is rebound with the data_sources of /v1/reports/update; this call is refused on it with 400 APPEND_NOT_ALLOWED and reason report_level_data_sources.\n\nIt writes two columns and reads the rest of the widget from its row: the definition, the format, the titles of its columns, its notes and its place are untouched. The cached results of the widget are dropped with the change, so it reads the new account at once.\n\nThe connection has to be one of the integration the widget already reads: the source of a widget that exists never changes. Every call keeps the widget as it was first and answers with the backup_key /v1/widgets/restore puts back, for 7 days -- that restore puts back the DEFINITION and leaves the connection the widget has at that moment, so the undo of this call is another call to it.\n\nRefusals: 400 for the framework and the body, 400 APPEND_NOT_ALLOWED with reason report_level_data_sources or template_based, 400 INVALID_VALUE when the widget reads no data source at all, 400 CONNECTION_NOT_FOUND or ACCOUNT_NOT_FOUND when the pair is not one /v1/connections publishes, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 WIDGET_NOT_EDITABLE when the widget reads several connected accounts at once (reason COMPARES_ACCOUNTS) or is a cross-platform widget that reads an accounts group, which brings the connections of its data sources (reason READS_ACCOUNTS_GROUP), 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), and 409 REPORT_IN_TRASH.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the set_widget_connection tool to point one widget of a report that binds per widget at another connection and account of its own integration. Take the pair from list_connections.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WidgetsSetConnectionRequest"
              },
              "examples": {
                "rebound": {
                  "summary": "One widget of a report that binds per widget, pointed at another ad account.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                    "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                    "connection_key": "facebook_ads_acme",
                    "account_id": "act_412298370015",
                    "idempotency_key": "2b7d4e10-91fa-4c68-8d05-31e7c9a6b402"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WidgetsSetConnectionResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/reports/update": {
      "post": {
        "operationId": "updateReport",
        "tags": [
          "Reports"
        ],
        "summary": "Change the settings of a report",
        "description": "Changes the fields of a report that already exists, the period of every one of its sections and the whole settings window of the report. It is a partial update: only the fields the body carries are written, and inside a block only the keys it carries, so everything it leaves out is left exactly as it is. /v1/reports/get publishes the same settings under `settings`, key by key, which is the object this call takes back.\n\n`date_range` is not a field of the report: the period belongs to each SECTION, and what this field does is what the application's own button does, applying one range to ALL of them at once. It writes the range and nothing else, so the header of each section goes on showing whatever it showed. The answer lists the sections it wrote, and the cached results of the whole report are dropped so the next read is of the new period.\n\n`data_sources` rebinds the report as a whole and is for the reports whose `report_level_data_sources` is true. On a report that binds a connection per widget it is refused with 400 APPEND_NOT_ALLOWED and reason per_widget_data_sources; those are rebound with /v1/reports/set-connections.\n\n`share` is the public link of the report. The id is minted by the call the first time the report is shared and again whenever `regenerate` is true, which invalidates the previous link; a password is stored hashed and `null` empties it. A template has no public link and the field is refused on one.\n\nThe settings window is the rest of the body. `brand_template` and `language_file` take an empty string for the system one. A template carries no brand of its own: on one `brand_template` takes only the empty string, and the key a template still stores is accepted and changes nothing. `front_cover` carries the type, the texts and the date line, and the TYPE commands the file: a type that prints an uploaded PDF is refused on a report that carries none, and a type that prints none removes the file the report carried, which the answer says with a COVER_FILE_REMOVED warning -- a dry run says it too -- and `before.front_cover.file`. `localization` either follows the account or belongs to the report: the six settings under it exist only in the second case, sending one of them with `use: account` is refused, and a call that writes that branch asks for the six together. `ai_brief` has three states: absent keeps it, an empty string removes it, a text replaces it.\n\nThe block /v1/reports/get answers under `settings` is this body, whole: sending it back writes what the report already has and changes nothing. A key carrying null means the column has no value and is left as it is; an empty string is a value. The three keys only a read can answer -- `brand_template_name`, `language_file_name` and `front_cover.file` -- are accepted and ignored, each with a READ_ONLY_FIELD_IGNORED warning naming it. `updated_fields` and `before` carry the fields the call really wrote, so a block that changes nothing appears in neither.\n\nChanging the language file, the localization mode or the date format drops the cached results of the report so the next read is of the new setting; nothing else drops them, and nothing is executed by this call.\n\nThe uploaded PDF of the cover is the one thing of that window this call does not write: it is a file, and it has a route of its own, /v1/reports/set-cover-file, which writes the cover type with it.\n\nWhat it never writes: the level at which the report binds its data sources (moving a report to per widget bindings deletes its binding rows), its identity, structure and provenance, and the settings of the ACCOUNT.\n\nThere is no guard against concurrent edits: the call writes over whatever the report holds when it lands, a change made in the application since the client read the report included.\n\nRefusals: 400 for the framework and the body, 400 APPEND_NOT_ALLOWED with reason per_widget_data_sources or template_based, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND and 409 REPORT_IN_TRASH.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the update_report tool to rename a report, change its subtitle or notes, publish or regenerate its public link, rebind its data sources, or move every one of its sections to a new period.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportsUpdateRequest"
              },
              "examples": {
                "new_month": {
                  "summary": "Every section of the report moved to last month.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "date_range": {
                      "preset": "lastmonth"
                    },
                    "idempotency_key": "6d1f0b52-7c48-4a3e-9d20-5b7e18c4a903"
                  }
                },
                "renamed_and_shared": {
                  "summary": "A new title and a public link protected by a password.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "name": "Acme - Performance September 2026",
                    "share": {
                      "enabled": true,
                      "password": "acme-2026",
                      "allow_date_range": false
                    },
                    "idempotency_key": "b1a7c0de-2f44-4c19-8a55-90e2d7b6f314"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportsUpdateResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/reports/set-connections": {
      "post": {
        "operationId": "setReportConnections",
        "tags": [
          "Reports"
        ],
        "summary": "Rebind every widget of a report",
        "description": "Points every widget of a report at another connection and another account, one pair per integration. It is for the reports that bind a connection PER WIDGET -- the ones whose `report_level_data_sources` is false -- and it is the bulk form of /v1/widgets/set-connection: each widget is loaded as it is stored, backed up, written with the two columns of the connection replaced, and its cached results are dropped so it reads the new account at once.\n\nA report that binds once for the whole report has no per widget connection to set and is refused with 400 APPEND_NOT_ALLOWED and reason report_level_data_sources; rebind it with the `data_sources` of /v1/reports/update. A report built on a template is refused with reason template_based, because its widgets belong to the template.\n\nIT IS NOT ATOMIC. Each widget is written on its own: the answer lists the widgets that were bound and the widgets that were not, each of the second kind with the reason -- its integration is not in the body, it reads several connected accounts at once, it is a cross-platform widget that reads an accounts group (whose connections it keeps), or its write failed. A widget that reads no data at all -- a text block, an image, a page break, an AI summary -- is in neither list. Retrying the same body finishes the job, because writing the connection a widget already has writes the same two values.\n\nRefusals: 400 for the framework and the body, 400 INVALID_VALUE when `connections` is empty, 400 CONNECTION_NOT_FOUND or ACCOUNT_NOT_FOUND when a pair is not one /v1/connections publishes, 400 APPEND_NOT_ALLOWED, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND and 409 REPORT_IN_TRASH.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the set_report_connections tool to point every widget of a report that binds per widget at this month's connections, one pair per integration.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportsSetConnectionsRequest"
              },
              "examples": {
                "new_month": {
                  "summary": "Two integrations rebound across the whole report.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "connections": [
                      {
                        "integration_id": "facebook_ads",
                        "connection_key": "facebook_ads_acme",
                        "account_id": "act_412298370015"
                      },
                      {
                        "integration_id": "google_ads",
                        "connection_key": "google_ads_acme",
                        "account_id": "482-113-9075"
                      }
                    ],
                    "idempotency_key": "9a4c2f11-6e07-4bb8-95d3-0c17e2a4d685"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportsSetConnectionsResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/reports/copy": {
      "post": {
        "operationId": "copyReport",
        "tags": [
          "Reports"
        ],
        "summary": "Copy a report",
        "description": "Duplicates a report under another name, as a report or as a template. The copy carries the sections, the widgets, the data source bindings, the AI summaries with their selections remapped to the new keys, and the AI client briefs. It carries no public share link (two reports cannot share one), no catalog provenance and no legacy mark, and it is stamped as created through the API.\n\nIt copies no schedule and touches no portal view: a copy is a report nobody is subscribed to yet. To copy a report FOR a client, with its own connections, schedule and portal view, use /v1/reports/duplicate-for-client.\n\nA report built on a template comes out as a regular report of its own, with copies of the sections it showed, and stops following that template; the answer says so in `warnings`.\n\nWith `as: template` the three refusals of the application apply -- a report built on a template is not saved as a template, neither is one that assigns a connection per widget, and neither is one with cross-platform widgets that read an accounts group, because every report built on the template would show the data of that group; the message names those widgets -- and the copy carries no brand. Neither does a template copied as a template: a template carries no brand of its own. A copy of a template or of a report built on one, and a copy that is a template, never keeps a cross-platform widget on an accounts group: in the copy it reads the connections of the report, and `warnings` names those widgets.\n\nThe plan limit is checked before anything is written: a call with no room left is refused with 403 PLAN_LIMIT_REACHED.\n\nRefusals: 400 for the framework and the body, 400 INVALID_VALUE for an `as` the source does not allow, 403 WRITE_ACCESS_DISABLED, 403 PLAN_LIMIT_REACHED, 404 NOT_FOUND and 409 REPORT_IN_TRASH.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the copy_report tool to duplicate a report under a new name, or to save it as a reusable template.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportsCopyRequest"
              },
              "examples": {
                "as_report": {
                  "summary": "The same report again, under another name.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "name": "Acme - Performance (copy)",
                    "idempotency_key": "4f77c1b8-0a2d-4e63-bb91-8e5d3c07a142"
                  }
                },
                "as_template": {
                  "summary": "The report saved as a reusable template.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "name": "Monthly performance template",
                    "as": "template",
                    "idempotency_key": "0c93a5e7-41bd-4f08-9a26-7d1e5b80c364"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportsCopyResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/reports/duplicate-for-client": {
      "post": {
        "operationId": "duplicateReportForClient",
        "tags": [
          "Reports"
        ],
        "summary": "Duplicate a report for another client",
        "description": "Copies a report and points it at another client's data, in one call: the copy is made, it is rebound, a schedule is created when the body carries one, and it is added to a portal view when the body names one.\n\nTHE SHAPE OF THE BODY FOLLOWS THE SOURCE. A report that binds its data sources once for the whole report is rebound with `data_sources`; one that binds a connection per widget with `connections`. Sending the other one is refused by name, because a rebinding that did nothing would hand the new client a report reading the previous client's accounts. `/v1/reports/get` publishes `report_level_data_sources` for every report.\n\nONLY THE FIRST STEP CAN REFUSE THE CALL. Once the copy exists nothing undoes it, so the answer reports the other three as they went: `bound[]` and `failed[]` per binding, `schedule` and `portal_view` each saying whether it was requested and whether it was done, with the `error_code` of what stopped it. A retry with the same `idempotency_key` answers this same result and never writes a second report.\n\nIt asks for a confirmation when the body carries `schedule` or `portal_view_key`: those are the two steps that reach people. The creation of a schedule counts against the daily budget of calls that send email, and so does adding the report to a portal view when that notifies its users.\n\nA report with cross-platform widgets that read an accounts group is refused with 400 INVALID_VALUE on `report_key`, naming those widgets: a copy for another client would keep showing the data of that group. Point them at the report connections in the editor first, or copy the report with /v1/reports/copy.\n\nRefusals: 400 for the framework and the body, 400 INVALID_VALUE or MISSING_REQUIRED_FIELD when the rebinding does not match the source or the report has widgets on an accounts group, 403 WRITE_ACCESS_DISABLED, 403 PLAN_LIMIT_REACHED, 404 NOT_FOUND, 409 REPORT_IN_TRASH and 429 EMAIL_QUOTA_REACHED.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the duplicate_report_for_client tool to copy a report for the next client, rebound to that client's connections, optionally scheduled and optionally added to a portal view.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportsDuplicateForClientRequest"
              },
              "examples": {
                "report_level": {
                  "summary": "A copy for a new client, rebound, scheduled and put in a portal view.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "name": "Globex - Monthly performance",
                    "data_sources": [
                      {
                        "integration_id": "google_ads",
                        "connection_key": "google_ads_globex",
                        "account_id": "771-204-8830"
                      }
                    ],
                    "schedule": {
                      "frequency": "monthly",
                      "frequency_details": "1",
                      "time": 8,
                      "recipients_to": [
                        "ana@globex.com"
                      ]
                    },
                    "portal_view_key": "3d5b8c1e90af42d7ae61cb04f78d2951",
                    "idempotency_key": "c2e84a17-93d5-4f60-8b72-1a0946ec5d38"
                  }
                },
                "per_widget": {
                  "summary": "A source that binds per widget is rebound with connections.",
                  "value": {
                    "report_key": "1b6e4d2c88af41539cd0726ea4b31f57",
                    "name": "Globex - Paid media",
                    "connections": [
                      {
                        "integration_id": "facebook_ads",
                        "connection_key": "facebook_ads_globex",
                        "account_id": "act_998877665544"
                      }
                    ],
                    "idempotency_key": "7f31d0a9-5c62-4e18-93ba-6d48e2c71b05"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportsDuplicateForClientResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/reports/set-cover-file": {
      "post": {
        "operationId": "setReportCoverFile",
        "tags": [
          "Reports"
        ],
        "summary": "Upload or remove the front cover PDF of a report",
        "description": "Uploads the PDF the front cover of a report prints, or removes the one it carries. It is the one thing of the settings window of a report that is a FILE and not a setting: everything else of that window -- the cover type, its texts, its date line, the brand, the localization -- is written by /v1/reports/update.\n\nTHE FILE AND THE COVER TYPE ARE WRITTEN TOGETHER, and this call resolves the type itself. Uploading a file moves the type to the one that prints it (`none` to `pdf`, `report` to `pdf_and_report`) and `file: null` moves it back to the one that prints none. The answer says so with a COVER_TYPE_SET warning whenever the type changed, and `updated_fields` names what really changed: `front_cover.file` and, when the type moved, `front_cover.type`. A report never holds a file under a type that does not print it -- the settings window of the application throws such a file away the next time it saves the report -- nor a type that prints one with no file, which that window refuses to save.\n\nTHE TWO LEGACY COVER TYPES MOVE NOWHERE. `pdf_and_custom` already prints an uploaded PDF and the application shows the upload field for it, so a REPLACEMENT file is written and the type stays `pdf_and_custom`: there is no COVER_TYPE_SET warning, `updated_fields` carries `front_cover.file` alone and the cover texts of the report are left as they are. What is refused, with 400 INVALID_VALUE and `details.reason` `legacy_cover_type`, is what would move the cover from one half of that legacy pair to the other: a file sent to a report whose cover is `custom` -- where the application does not even show the upload -- and `file: null` on a `pdf_and_custom` one, whose destination would be `custom`. Neither move is one the application or /v1/reports/update makes; change the cover type in the application instead.\n\nTHE PREVIOUS FILE IS NOT KEPT. No backup is taken of it, so putting back a file this call replaced or removed means uploading it again; `before` names the file that went and the type the report had. Nothing is dropped from the cache: a front cover is drawn when the PDF is built and is not a widget anybody executed.\n\nA report built on a template is accepted: the front cover it prints is its own row and not its template's.\n\nRefusals: 400 for the framework and the body, 400 INVALID_VALUE, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 REPORT_IN_TRASH and 413 PAYLOAD_TOO_LARGE.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.\n\nA file that is refused is 400 INVALID_VALUE with the rule that stopped it in `details.reason`, one of: `too_large` (over the ceiling of the account, 262144 decoded bytes, 524288 for the few accounts that were given the larger one; the message names the one that was applied) and `not_a_pdf` (the first five bytes of the file are not `%PDF-`, whatever the file is called). THIS IS STRICTER THAN THE APPLICATION, whose upload looks at the extension of the file name and nothing else, and stricter than the reader that draws the cover: the header is asked for at the start of the file, so a PDF preceded by a byte order mark or by padding is refused here although the product would draw it. NOTHING OPENS THE DOCUMENT: a file that is a PDF only in its first bytes is accepted, stored, and fails later, when the report is built.",
        "x-mcp-description": "Use the set_report_cover_file tool to upload or remove the PDF the front cover of a report prints. The client encodes the file; never generate the bytes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportsSetCoverFileRequest"
              },
              "examples": {
                "uploaded": {
                  "summary": "The cover of this month's report (the base64 is cut short here).",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "file": {
                      "name": "cover-september-2026.pdf",
                      "content_base64": "JVBERi0xLjQKJeLjz9MK"
                    },
                    "idempotency_key": "0b39f5c8-6e27-4a14-b8d0-53f7e91ca264"
                  }
                },
                "removed": {
                  "summary": "Take the uploaded cover away; the type goes back to the one that prints none.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "file": null,
                    "idempotency_key": "6d1a04b7-25e8-4c33-9f02-7b1e58c4a90d"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportsSetCoverFileResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/sections/delete": {
      "post": {
        "operationId": "deleteSection",
        "tags": [
          "Sections"
        ],
        "summary": "Delete a section of a report",
        "description": "Deletes one section with every widget it holds, and moves the version of the report's sections structure. A report keeps at least one section: deleting the last one is refused with 400 INVALID_VALUE. There is no undo. This endpoint ALWAYS asks for a confirmation, and its preview lists the widgets that would go, how many sections would be left and whether the deletion reaches the reports built on this one. Deleting a section of a TEMPLATE does reach them: the section, with its widgets, disappears from every report built on that template. A report built on a template has no sections of its own -- they belong to its template -- and is refused with 400 APPEND_NOT_ALLOWED, reason template_based. It accepts if_version, and the guard is checked again inside the deleting transaction, so a section appended in between cannot be deleted by mistake. Refusals: 400 INVALID_VALUE, 400 APPEND_NOT_ALLOWED or the framework ones, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 VERSION_MISMATCH plus the confirmation and idempotency conflicts.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the delete_section tool to undo an append_sections. It always confirms first, and the preview lists every widget that would go with the section.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SectionsDeleteRequest"
              },
              "examples": {
                "confirm_then_delete": {
                  "summary": "Guarded with the version the report was read at. The first call answers a token; the second is this same body with confirmation_token added.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "section_key": "e13f97b25a684cd0b7e4a8236f95d108",
                    "if_version": 8,
                    "idempotency_key": "delete-creatives-section-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SectionsDeleteResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The second call, with the token. The report moved to version 9.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "section_key": "e13f97b25a684cd0b7e4a8236f95d108",
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "version": 9,
                        "deleted": true,
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run": {
                    "summary": "dry_run: true. Nothing is deleted.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "dry_run": true,
                        "valid": true,
                        "section_key": "e13f97b25a684cd0b7e4a8236f95d108",
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "name": "Creatives",
                        "version": 8,
                        "widgets": 1,
                        "sections_left": 2,
                        "will_delete_widgets": [
                          {
                            "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                            "name": "Spend by campaign",
                            "row": 1,
                            "column": 1,
                            "integration_id": "facebook_ads"
                          }
                        ],
                        "propagates_to_template_based_reports": false,
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run_invalid": {
                    "$ref": "#/components/examples/WriteDryRunInvalid"
                  },
                  "confirmation_required": {
                    "$ref": "#/components/examples/WriteConfirmationRequired"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/schedules/create": {
      "post": {
        "operationId": "createSchedule",
        "tags": [
          "Schedules"
        ],
        "summary": "Schedule a report for delivery by email",
        "description": "Configures the standing delivery of a report by email, or sends it once with frequency now. A report has at most one standing schedule: when it already has an active one the call is refused with 409 SCHEDULE_ALREADY_EXISTS, and when it has a stopped one this call reconfigures it and shows what it looked like before. The message comes either from an email template of the account or from the six email fields, never from both. A template cannot be scheduled. This endpoint ALWAYS asks for a confirmation, and it spends one unit of the daily budget of calls that send email. Refusals: 400 VALIDATION_FAILED, UNKNOWN_PARAM, MISSING_REQUIRED_FIELD or INVALID_VALUE, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND for the report, the email template or the language file it names, 409 SCHEDULE_ALREADY_EXISTS, SCHEDULE_ALREADY_QUEUED, SCHEDULE_QUEUED, NOT_EDITABLE_BY_API, SCHEDULE_USES_SYSTEM_TEMPLATE (when the stopped schedule this call would configure is built on the system email template) or ACCOUNT_HAS_NO_OWNER plus the confirmation and idempotency conflicts, 429 EMAIL_QUOTA_REACHED. Two warnings travel in a successful answer: REPORT_WILL_BE_SHARED_PUBLICLY when the link format will create and enable a public link without a password, and MAY_BE_DELIVERED_AGAIN_TODAY when the hour of the schedule has already passed today.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the create_schedule tool to set up the recurring email of a report, or to send it once with frequency now. It always confirms first, and the preview shows exactly who would receive it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SchedulesCreateRequest"
              },
              "examples": {
                "weekly": {
                  "summary": "A weekly delivery with its own email text.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "frequency": "weekly",
                    "frequency_details": "MONDAY",
                    "time": "08:00",
                    "recipients_to": [
                      "client@example.com"
                    ],
                    "report_format": "pdf",
                    "format": "html",
                    "sender": "Studio Reports",
                    "sender_email": "reports@example.com",
                    "reply_to_email": "reports@example.com",
                    "subject": "Your weekly report",
                    "body": "<p>Hello, here is the report of the week.</p>",
                    "idempotency_key": "schedule-meta-monthly-weekly-1"
                  }
                },
                "send_now": {
                  "summary": "A one-off delivery. No time is read, and nothing is left behind.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "frequency": "now",
                    "recipients_to": [
                      "client@example.com"
                    ],
                    "report_format": "pdf",
                    "format": "html",
                    "sender": "Studio Reports",
                    "sender_email": "reports@example.com",
                    "reply_to_email": "reports@example.com",
                    "subject": "Your report",
                    "body": "<p>Here is the report you asked for.</p>",
                    "idempotency_key": "send-meta-monthly-now-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SchedulesCreateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The second call, with the token. The schedule is configured.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "schedule_key": "2e8b6d34fa9147c5b03e7a1c986df205",
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "frequency": "weekly",
                        "frequency_details": "MONDAY",
                        "time": "08:00",
                        "recipients_to": [
                          "client@example.com"
                        ],
                        "recipients_cc": [],
                        "recipients_bcc": [],
                        "report_format": "pdf",
                        "email_template_key": null,
                        "uses_system_template": false,
                        "format": "html",
                        "subject": "Your weekly report",
                        "sender": "Studio Reports",
                        "sender_email": "reports@example.com",
                        "reply_to_email": "reports@example.com",
                        "origin": "api",
                        "user_key": "8b0d3f6a94c247e5b81a6d20fc739e14",
                        "conditional_execution_enabled": false,
                        "queued": false,
                        "report_name": "Meta Ads -- monthly",
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run": {
                    "summary": "The same body with dry_run: true. Nothing is written; `after` is the schedule that would result.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "dry_run": true,
                        "valid": true,
                        "schedule_key": null,
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "report_name": "Meta Ads -- monthly",
                        "after": {
                          "schedule_key": null,
                          "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                          "frequency": "weekly",
                          "frequency_details": "MONDAY",
                          "time": "08:00",
                          "recipients_to": [
                            "client@example.com"
                          ],
                          "recipients_cc": [],
                          "recipients_bcc": [],
                          "report_format": "pdf",
                          "email_template_key": null,
                          "uses_system_template": false,
                          "format": "html",
                          "subject": "Your weekly report",
                          "sender": "Studio Reports",
                          "sender_email": "reports@example.com",
                          "reply_to_email": "reports@example.com",
                          "origin": "api",
                          "user_key": "8b0d3f6a94c247e5b81a6d20fc739e14",
                          "conditional_execution_enabled": false,
                          "queued": false
                        },
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run_invalid": {
                    "$ref": "#/components/examples/WriteDryRunInvalid"
                  },
                  "confirmation_required": {
                    "$ref": "#/components/examples/WriteConfirmationRequired"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/schedules/update": {
      "post": {
        "operationId": "updateSchedule",
        "tags": [
          "Schedules"
        ],
        "summary": "Change a scheduled delivery",
        "description": "Changes a scheduled delivery. The update is partial -- only the fields the body carries are laid over the stored row -- and the result is validated as a whole, so a change that would leave an incomplete row is refused. Two rows cannot be updated at all and answer 409: one whose delivery depends on a condition (NOT_EDITABLE_BY_API) and one built from the system email template (SCHEDULE_USES_SYSTEM_TEMPLATE); a schedule created from the client portal is refused the same way. The confirmation is CONDITIONAL: it is asked for only when the body touches the recipients, the frequency, the day of the week or of the month, the hour, the attachment format, the email template, the body format or any of the six email fields -- that is, everything except report_key. if_version is not accepted. Refusals: 400 VALIDATION_FAILED, UNKNOWN_PARAM or INVALID_VALUE, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 NOT_EDITABLE_BY_API, SCHEDULE_USES_SYSTEM_TEMPLATE, SCHEDULE_QUEUED or ACCOUNT_HAS_NO_OWNER plus the confirmation and idempotency conflicts. It does NOT spend the daily email budget, so it never answers EMAIL_QUOTA_REACHED. The same two warnings a creation can carry apply here.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the update_schedule tool to change an existing delivery -- the hour, the recipients, the frequency. It confirms first only when the change touches who gets the email or when it goes out.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SchedulesUpdateRequest"
              },
              "examples": {
                "move_the_hour": {
                  "summary": "Only the hour changes; everything else keeps the stored value. This touches a confirmation trigger, so the first call answers a token.",
                  "value": {
                    "schedule_key": "2e8b6d34fa9147c5b03e7a1c986df205",
                    "time": "07:00",
                    "idempotency_key": "schedule-meta-monthly-move-hour-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SchedulesUpdateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The second call, with the token. `before` is the schedule as it was.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "schedule_key": "2e8b6d34fa9147c5b03e7a1c986df205",
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "frequency": "weekly",
                        "frequency_details": "MONDAY",
                        "time": "07:00",
                        "recipients_to": [
                          "client@example.com"
                        ],
                        "recipients_cc": [],
                        "recipients_bcc": [],
                        "report_format": "pdf",
                        "email_template_key": null,
                        "uses_system_template": false,
                        "format": "html",
                        "subject": "Your weekly report",
                        "sender": "Studio Reports",
                        "sender_email": "reports@example.com",
                        "reply_to_email": "reports@example.com",
                        "origin": "api",
                        "user_key": "8b0d3f6a94c247e5b81a6d20fc739e14",
                        "conditional_execution_enabled": false,
                        "queued": false,
                        "report_name": "Meta Ads -- monthly",
                        "before": {
                          "schedule_key": "2e8b6d34fa9147c5b03e7a1c986df205",
                          "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                          "frequency": "weekly",
                          "frequency_details": "MONDAY",
                          "time": "08:00",
                          "recipients_to": [
                            "client@example.com"
                          ],
                          "recipients_cc": [],
                          "recipients_bcc": [],
                          "report_format": "pdf",
                          "email_template_key": null,
                          "uses_system_template": false,
                          "format": "html",
                          "subject": "Your weekly report",
                          "sender": "Studio Reports",
                          "sender_email": "reports@example.com",
                          "reply_to_email": "reports@example.com",
                          "origin": "api",
                          "user_key": "8b0d3f6a94c247e5b81a6d20fc739e14",
                          "conditional_execution_enabled": false,
                          "queued": false,
                          "last_execution_at": "2026-09-14T06:00:11Z"
                        },
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run": {
                    "summary": "The same body with dry_run: true.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "dry_run": true,
                        "valid": true,
                        "schedule_key": "2e8b6d34fa9147c5b03e7a1c986df205",
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "report_name": "Meta Ads -- monthly",
                        "before": {
                          "schedule_key": "2e8b6d34fa9147c5b03e7a1c986df205",
                          "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                          "frequency": "weekly",
                          "frequency_details": "MONDAY",
                          "time": "08:00",
                          "recipients_to": [
                            "client@example.com"
                          ],
                          "recipients_cc": [],
                          "recipients_bcc": [],
                          "report_format": "pdf",
                          "email_template_key": null,
                          "uses_system_template": false,
                          "format": "html",
                          "subject": "Your weekly report",
                          "sender": "Studio Reports",
                          "sender_email": "reports@example.com",
                          "reply_to_email": "reports@example.com",
                          "origin": "api",
                          "user_key": "8b0d3f6a94c247e5b81a6d20fc739e14",
                          "conditional_execution_enabled": false,
                          "queued": false,
                          "last_execution_at": "2026-09-14T06:00:11Z"
                        },
                        "after": {
                          "schedule_key": "2e8b6d34fa9147c5b03e7a1c986df205",
                          "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                          "frequency": "weekly",
                          "frequency_details": "MONDAY",
                          "time": "07:00",
                          "recipients_to": [
                            "client@example.com"
                          ],
                          "recipients_cc": [],
                          "recipients_bcc": [],
                          "report_format": "pdf",
                          "email_template_key": null,
                          "uses_system_template": false,
                          "format": "html",
                          "subject": "Your weekly report",
                          "sender": "Studio Reports",
                          "sender_email": "reports@example.com",
                          "reply_to_email": "reports@example.com",
                          "origin": "api",
                          "user_key": "8b0d3f6a94c247e5b81a6d20fc739e14",
                          "conditional_execution_enabled": false,
                          "queued": false
                        },
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run_invalid": {
                    "$ref": "#/components/examples/WriteDryRunInvalid"
                  },
                  "confirmation_required": {
                    "$ref": "#/components/examples/WriteConfirmationRequired"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/schedules/delete": {
      "post": {
        "operationId": "deleteSchedule",
        "tags": [
          "Schedules"
        ],
        "summary": "Stop a scheduled delivery",
        "description": "Stops a scheduled delivery. The configuration is NOT destroyed: the schedule is put at frequency none, so it stays listed and /v1/schedules/update can turn it back on.\n\nIT IS ALSO THE INVERSE OF /v1/reports/take-snapshot. Sent with the `schedule_key` that call answered, it cancels the snapshot while the platform has not taken it yet. That row is the request of one file and not a configuration anybody keeps, so there it is REMOVED and not put at frequency none: `row_is_kept` says false and `queued_snapshot_cancelled` says true. Once the snapshot has been taken the key resolves to nothing and the answer is 404; what is deleted then is the file, with /v1/snapshots/delete. This endpoint ALWAYS asks for a confirmation, and its preview says whether the schedule was actually delivering anything. Unlike an update, it accepts the schedules this version cannot express: a conditional delivery, one built on the system email template and one created from the client portal can all be stopped. if_version is not accepted. Refusals: 400 INVALID_VALUE (an explicit null report_key) and the framework ones, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND when the schedule is not this account's, is a one-off delivery of an email or does not deliver the report_key that was sent, 409 SCHEDULE_QUEUED while the delivery is being sent, plus the confirmation and idempotency conflicts.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the delete_schedule tool to stop a delivery. The schedule is kept at frequency none, so update_schedule can turn it back on later.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SchedulesDeleteRequest"
              },
              "examples": {
                "confirm_then_stop": {
                  "summary": "The first call answers a token; the second is this same body with confirmation_token added.",
                  "value": {
                    "schedule_key": "2e8b6d34fa9147c5b03e7a1c986df205",
                    "idempotency_key": "stop-meta-monthly-weekly-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SchedulesDeleteResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The second call, with the token. The delivery is stopped.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "schedule_key": "2e8b6d34fa9147c5b03e7a1c986df205",
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "frequency": "none",
                        "queued_snapshot_cancelled": false,
                        "row_is_kept": true,
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run": {
                    "summary": "dry_run: true. Nothing is stopped.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "dry_run": true,
                        "valid": true,
                        "schedule_key": "2e8b6d34fa9147c5b03e7a1c986df205",
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "before": {
                          "schedule_key": "2e8b6d34fa9147c5b03e7a1c986df205",
                          "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                          "frequency": "weekly",
                          "frequency_details": "MONDAY",
                          "time": "08:00",
                          "recipients_to": [
                            "client@example.com"
                          ],
                          "recipients_cc": [],
                          "recipients_bcc": [],
                          "report_format": "pdf",
                          "email_template_key": null,
                          "uses_system_template": false,
                          "format": "html",
                          "subject": "Your weekly report",
                          "sender": "Studio Reports",
                          "sender_email": "reports@example.com",
                          "reply_to_email": "reports@example.com",
                          "origin": "api",
                          "user_key": "8b0d3f6a94c247e5b81a6d20fc739e14",
                          "conditional_execution_enabled": false,
                          "queued": false,
                          "last_execution_at": "2026-09-14T06:00:11Z"
                        },
                        "stops_delivery": true,
                        "cancels_queued_snapshot": false,
                        "row_is_kept": true,
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run_invalid": {
                    "$ref": "#/components/examples/WriteDryRunInvalid"
                  },
                  "confirmation_required": {
                    "$ref": "#/components/examples/WriteConfirmationRequired"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/portal-views/create": {
      "post": {
        "operationId": "createPortalView",
        "tags": [
          "Portal Views"
        ],
        "summary": "Create a view of the client portal",
        "description": "Creates a view of the client portal: which reports which client users see, in which order, with which brand and with which choice of periods. Both lists are required and neither may be empty. With send_notification the users are emailed, and that email is sent INSIDE the transaction that writes the view: a send that fails takes the write with it and answers 502 NOTIFICATION_FAILED, so nothing is left half done. The confirmation is CONDITIONAL: it is asked for only when the call would actually email somebody. Refusals: 400 VALIDATION_FAILED, UNKNOWN_PARAM, MISSING_REQUIRED_FIELD or INVALID_VALUE (a template among the reports, a staff user among the users, a repeat in either list), 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND for a report, a user or a brand template, 409 PORTAL_NOT_CONFIGURED when the portal has no subdomain or no email template plus the confirmation and idempotency conflicts, 429 EMAIL_QUOTA_REACHED, 502 NOTIFICATION_FAILED. if_version is not accepted.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the create_portal_view tool to give a client access to the reports you just built. It confirms first whenever it would email somebody.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PortalViewsCreateRequest"
              },
              "examples": {
                "with_notification": {
                  "summary": "One report, one client user, and an email announcing it. Because it notifies, the first call answers a token.",
                  "value": {
                    "name": "Acme",
                    "internal_notes": "Renewal in November",
                    "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                    "send_notification": true,
                    "allow_to_change_date_range": true,
                    "date_range_option": "last_monthly_periods",
                    "report_keys": [
                      "9f3c1a7e42b84d0596ce1f8a3b7d2e64"
                    ],
                    "user_keys": [
                      "8b0d3f6a94c247e5b81a6d20fc739e14"
                    ],
                    "idempotency_key": "portal-view-acme-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortalViewsCreateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The second call, with the token. The view exists and one address was emailed.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "portal_view_key": "5c94a7e208f34b16bd7e03c2a951f687",
                        "name": "Acme",
                        "internal_notes": "Renewal in November",
                        "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                        "send_notification": true,
                        "allow_to_change_date_range": true,
                        "date_range_option": "last_monthly_periods",
                        "report_keys": [
                          "9f3c1a7e42b84d0596ce1f8a3b7d2e64"
                        ],
                        "user_keys": [
                          "8b0d3f6a94c247e5b81a6d20fc739e14"
                        ],
                        "notified": [
                          "ana.reyes@example.com"
                        ],
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run": {
                    "summary": "The same body with dry_run: true. Nothing is written and nobody is emailed; will_notify says who would be.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "dry_run": true,
                        "valid": true,
                        "portal_view_key": null,
                        "after": {
                          "portal_view_key": "5c94a7e208f34b16bd7e03c2a951f687",
                          "name": "Acme",
                          "internal_notes": "Renewal in November",
                          "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                          "send_notification": true,
                          "allow_to_change_date_range": true,
                          "date_range_option": "last_monthly_periods",
                          "report_keys": [
                            "9f3c1a7e42b84d0596ce1f8a3b7d2e64"
                          ],
                          "user_keys": [
                            "8b0d3f6a94c247e5b81a6d20fc739e14"
                          ]
                        },
                        "will_notify": [
                          "ana.reyes@example.com"
                        ],
                        "notification_note": "The notifications are sent inside the transaction that writes the view, before it commits: recipients already notified are not un-notified if the write fails afterwards.",
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run_invalid": {
                    "$ref": "#/components/examples/WriteDryRunInvalid"
                  },
                  "confirmation_required": {
                    "$ref": "#/components/examples/WriteConfirmationRequired"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/NotificationFailed"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/portal-views/update": {
      "post": {
        "operationId": "updatePortalView",
        "tags": [
          "Portal Views"
        ],
        "summary": "Change a view of the client portal",
        "description": "Changes a view of the client portal. The update is partial -- a field the body does not carry keeps its value -- with one exception worth reading twice: report_keys and user_keys REPLACE the stored lists, they do not add to them, and neither may end up empty. A view whose reports are filtered globally by something other than nothing is refused with 409 NOT_EDITABLE_BY_API: this version could not show that filter in the before and after. The confirmation is CONDITIONAL, and it is the PRESENCE of a field that triggers it: a body carrying report_keys, user_keys or send_notification asks for a confirmation even when the values it sends are the ones already stored, and so does any call that would email somebody. Refusals: the same as the creation, plus NOT_EDITABLE_BY_API. if_version is not accepted.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the update_portal_view tool to add a report to what a client already sees. Send the WHOLE list of report_keys, not just the new one: the list replaces the stored one.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PortalViewsUpdateRequest"
              },
              "examples": {
                "replace_the_reports": {
                  "summary": "The whole list of reports, not just the new one. It touches report_keys, so the first call answers a token.",
                  "value": {
                    "portal_view_key": "5c94a7e208f34b16bd7e03c2a951f687",
                    "report_keys": [
                      "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                      "4d81b0e6c5a74f3298ad6e2b9c105f37"
                    ],
                    "idempotency_key": "portal-view-acme-add-ppc-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortalViewsUpdateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The second call, with the token. Nobody was emailed.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "portal_view_key": "5c94a7e208f34b16bd7e03c2a951f687",
                        "name": "Acme",
                        "internal_notes": "Renewal in November",
                        "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                        "send_notification": true,
                        "allow_to_change_date_range": true,
                        "date_range_option": "last_monthly_periods",
                        "report_keys": [
                          "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                          "4d81b0e6c5a74f3298ad6e2b9c105f37"
                        ],
                        "user_keys": [
                          "8b0d3f6a94c247e5b81a6d20fc739e14"
                        ],
                        "before": {
                          "portal_view_key": "5c94a7e208f34b16bd7e03c2a951f687",
                          "name": "Acme",
                          "internal_notes": "Renewal in November",
                          "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                          "send_notification": true,
                          "allow_to_change_date_range": true,
                          "date_range_option": "last_monthly_periods",
                          "report_keys": [
                            "9f3c1a7e42b84d0596ce1f8a3b7d2e64"
                          ],
                          "user_keys": [
                            "8b0d3f6a94c247e5b81a6d20fc739e14"
                          ]
                        },
                        "notified": [],
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run": {
                    "summary": "The same body with dry_run: true.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "dry_run": true,
                        "valid": true,
                        "portal_view_key": "5c94a7e208f34b16bd7e03c2a951f687",
                        "before": {
                          "portal_view_key": "5c94a7e208f34b16bd7e03c2a951f687",
                          "name": "Acme",
                          "internal_notes": "Renewal in November",
                          "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                          "send_notification": true,
                          "allow_to_change_date_range": true,
                          "date_range_option": "last_monthly_periods",
                          "report_keys": [
                            "9f3c1a7e42b84d0596ce1f8a3b7d2e64"
                          ],
                          "user_keys": [
                            "8b0d3f6a94c247e5b81a6d20fc739e14"
                          ]
                        },
                        "after": {
                          "portal_view_key": "5c94a7e208f34b16bd7e03c2a951f687",
                          "name": "Acme",
                          "internal_notes": "Renewal in November",
                          "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                          "send_notification": true,
                          "allow_to_change_date_range": true,
                          "date_range_option": "last_monthly_periods",
                          "report_keys": [
                            "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                            "4d81b0e6c5a74f3298ad6e2b9c105f37"
                          ],
                          "user_keys": [
                            "8b0d3f6a94c247e5b81a6d20fc739e14"
                          ]
                        },
                        "will_notify": [],
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run_invalid": {
                    "$ref": "#/components/examples/WriteDryRunInvalid"
                  },
                  "confirmation_required": {
                    "$ref": "#/components/examples/WriteConfirmationRequired"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/NotificationFailed"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/users/create": {
      "post": {
        "operationId": "createClientUser",
        "tags": [
          "Users"
        ],
        "summary": "Create a client user",
        "description": "Creates a user of the client portal and sends him a welcome email with a generated password. The password is never published in the answer. The role is always client: the owner and the administrators of the account are not created here. An email address belongs to one user across the whole product, so an address already in use is refused with 409 USER_EMAIL_EXISTS. This endpoint ALWAYS asks for a confirmation -- there is no rehearsal of an email that has gone out -- and it spends one unit of the daily budget of calls that send email. Refusals: 400 VALIDATION_FAILED, UNKNOWN_PARAM, MISSING_REQUIRED_FIELD or INVALID_VALUE, 403 WRITE_ACCESS_DISABLED, 409 USER_EMAIL_EXISTS or PORTAL_NOT_CONFIGURED plus the confirmation and idempotency conflicts, 429 EMAIL_QUOTA_REACHED. The check that the portal can receive the user reads the account itself, so this endpoint declares 404 NOT_FOUND for the same defensive reason /v1/capabilities does. A user whose welcome email did not go out is still created and says so with the warning WELCOME_EMAIL_FAILED; he can use 'Forgot password' on the portal. if_version is not accepted.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the create_client_user tool to give a person access to the client portal before putting him in a portal view. It always confirms first, because it sends a welcome email that cannot be taken back.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UsersCreateRequest"
              },
              "examples": {
                "confirm_then_create": {
                  "summary": "The first call answers a token; the second is this same body with confirmation_token added.",
                  "value": {
                    "email": "ana.reyes@example.com",
                    "first_name": "Ana",
                    "last_name": "Reyes",
                    "language": "es",
                    "idempotency_key": "client-user-ana-reyes-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsersCreateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The second call, with the token. The user exists and the welcome email went out.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "user_key": "8b0d3f6a94c247e5b81a6d20fc739e14",
                        "email": "ana.reyes@example.com",
                        "first_name": "Ana",
                        "last_name": "Reyes",
                        "language": "es",
                        "role": "client",
                        "portal_url": "https://acme.reportingninja.com",
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run": {
                    "summary": "dry_run: true. Nothing is created and no email goes out.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "dry_run": true,
                        "valid": true,
                        "user_key": null,
                        "email": "ana.reyes@example.com",
                        "first_name": "Ana",
                        "last_name": "Reyes",
                        "language": "es",
                        "role": "client",
                        "will_send_email_to": "ana.reyes@example.com",
                        "portal_url": "https://acme.reportingninja.com",
                        "password": "generated by the server and sent in that email; it is never published here",
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-18T10:30:00Z"
                      }
                    }
                  },
                  "dry_run_invalid": {
                    "$ref": "#/components/examples/WriteDryRunInvalid"
                  },
                  "confirmation_required": {
                    "$ref": "#/components/examples/WriteConfirmationRequired"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/widgets/move": {
      "post": {
        "operationId": "moveWidget",
        "tags": [
          "Widgets"
        ],
        "summary": "Move a widget",
        "description": "Moves one widget to another row, another column or another section of the same report, or several widgets at once.\n\nRows and columns are numbered from 1 and every read publishes the ones a widget has. Send `to` with widget_key for one widget: a row past the last one of the section opens a new row, and a call without a column puts the widget at the end of the row it lands in. Send `layout` to place several at once, which is what the previous_layout of an earlier move is for: the answer publishes it in this very shape, so sending that array back as `layout` returns the report to the arrangement it had, in one call and with nothing to reshape. Every widget of the touched sections that `layout` does not name keeps the place it has.\n\nThe rules of the layout are the application's: a row holds up to 4 KPIs or up to 2 other widgets and never both, a widget whose definition asked for full_width is alone in its row, and a row holding a page break takes nobody else -- a page break, for its part, opens a row of its own anywhere and joins none. A row that does not take the widget is refused with 409 ROW_RULE, naming the widget the call placed and the first free row of the section. Only the rows this call puts a widget into are judged: a row written before these rules existed is left exactly as it is.\n\nA widget that ends up in another section is re-read against that section's date range and global filters: its cached results are dropped and the answer says so with section_changed. An AI summary crossing into another section answers to the same ceilings as one created there. A report built on a template is refused with 400 APPEND_NOT_ALLOWED, reason template_based: its layout belongs to its template.\n\nSend if_position to be refused with 409 POSITION_CHANGED when the widget is not where it was read, and retry with the new position AND THE SAME idempotency_key.\n\nRefusals: 400 for the framework and the body, 400 APPEND_NOT_ALLOWED, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 ROW_RULE, 409 POSITION_CHANGED, 409 AI_SUMMARY_NOT_ALLOWED, 409 SECTION_BUSY, 409 SECTION_CHANGED -- a section the call touches changed after the call read it, in which case nothing is written, so read the report again and send what is left -- and 409 REPORT_IN_TRASH.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the move_widget tool to change the row, the column or the section of a widget, and its previous_layout to put a report back the way it was.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WidgetsMoveRequest"
              },
              "examples": {
                "one": {
                  "summary": "One widget into the first row of another section.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                    "to": {
                      "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                      "row": 1,
                      "column": 1
                    },
                    "idempotency_key": "6f1c0b8e-2d54-4a71-9c3e-7b8a0d5f1e24"
                  }
                },
                "undo": {
                  "summary": "The previous_layout of that move, replayed.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "layout": [
                      {
                        "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                        "section_key": "b1d7f0925ac3482ea6e9c4d81f5307ab",
                        "row": 2,
                        "column": 1
                      },
                      {
                        "widget_key": "3e8b6a4c07d94f21ba5e9c380d71f46a",
                        "section_key": "c6a04e83d71b429fa58d2c9e37016b52",
                        "row": 1,
                        "column": 1
                      }
                    ],
                    "idempotency_key": "0b2f7d61-83ac-4e5f-9d10-2c7a4e6b8f30"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WidgetsMoveResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/widgets/copy": {
      "post": {
        "operationId": "copyWidget",
        "tags": [
          "Widgets"
        ],
        "summary": "Copy a widget",
        "description": "Copies one widget. The copy takes a row of its own right below the original and everything under it moves one row down, which is what the copy button of the application does; `to` puts it in another section or opens its row somewhere else.\n\nThe copy carries everything the original has -- its definition and its format, the connection it reads through, its internal notes, and for an AI summary its client brief and what it reads, pointed at the section the copy lands in. From then on the two are separate widgets: changing one does not change the other. Writing a variant of a widget that already works is a copy followed by an update of the copy.\n\nAn image and a page break are not copied -- the application offers no copy for them either -- and a report built on a template is refused with 400 APPEND_NOT_ALLOWED, reason template_based. A section holds at most 12 widgets.\n\nIt takes no backup and drops no cached result: the inverse of a copy is deleting the copy, which is also what undoes a copy that only got half way -- the answer declares atomic false, because creating the row, remapping what a copied summary reads, renumbering the section and copying the client brief are four writes.\n\nRefusals: 400 for the framework and the body, 400 APPEND_NOT_ALLOWED, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 AI_SUMMARY_NOT_ALLOWED, 409 SECTION_BUSY, 409 SECTION_CHANGED when the section the copy lands in changed after the call read it, in which case nothing is written and the call can be retried after reading the report again, and 409 REPORT_IN_TRASH.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the copy_widget tool to duplicate a widget, then update_widget on the copy to turn it into the variant you want.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WidgetsCopyRequest"
              },
              "examples": {
                "below": {
                  "summary": "A copy right below the original.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                    "idempotency_key": "1c8d3f70-45ab-4e92-8f61-0d5b7a2e9c34"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WidgetsCopyResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/widgets/create-image": {
      "post": {
        "operationId": "createImageWidget",
        "tags": [
          "Widgets"
        ],
        "summary": "Add a picture to a section",
        "description": "Writes one picture -- a logo, a screenshot, a diagram -- as one more widget of a section.\n\nIT IS THE ONE WIDGET THAT IS A FILE AND NOT A DEFINITION, which is why it has a route of its own: there is nothing about a picture that the widget DSL could describe. /v1/reports/get answers `type: image` and `editable: false` for it, with the size of the picture and the weight of its file under `image`, and never the bytes.\n\n`image_base64` carries the file itself, base64 encoded and with no data URI prefix, up to 716800 bytes DECODED. That ceiling is lower than the 1 MB the application accepts from an upload because these bytes travel encoded inside the request body, which is capped at 1 MB: a bigger one is answered 413 before this endpoint sees it.\n\nTHE BYTES DECIDE WHAT THE FILE IS. The size, the first bytes (JPEG, PNG or GIF) and finally whether the picture really decodes are all checked, in that order, whatever the filename says; anything else is refused by name. A file that is refused is 400 INVALID_VALUE with the rule that stopped it in `details.reason`, one of: `too_large` (over 716800 decoded bytes), `too_many_pixels` (the header announces more than 25000000 pixels, which is a picture nothing draws and a decode nothing should pay for), `format_not_supported` (the first bytes are not a JPEG, a PNG or a GIF, whatever the file is called) and `not_an_image` (the bytes announce a format and do not decode as one). The width and the height are read off the picture and are not chosen by the caller, exactly as in the application, and they are read from the HEADER first: a file of a few hundred KB can announce twenty thousand pixels a side and decode into hundreds of megabytes, so the ceiling of pixels is applied before anything is decoded -- on a dry run too, which validates everything.\n\nWithout a `placement` the widget goes to the first row that takes it. An image is not a KPI, so it counts as one of the elements a row holds; a row holding a page break takes nobody else. A report built on a template is refused with 400 APPEND_NOT_ALLOWED, reason template_based.\n\nRefusals: 400 for the framework and the body, 400 INVALID_VALUE, 400 APPEND_NOT_ALLOWED, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 VERSION_MISMATCH, 409 SECTION_BUSY, 409 REPORT_IN_TRASH and 413 PAYLOAD_TOO_LARGE.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the create_image_widget tool to put a file the user gave you into a report. The client encodes the file; never generate the bytes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WidgetsCreateImageRequest"
              },
              "examples": {
                "logo": {
                  "summary": "A PNG at the head of a section (the base64 is cut short here).",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "section_key": "5b2e8c14a9f04d3786c1e0b7d5493fa2",
                    "image_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
                    "filename": "logo.png",
                    "placement": {
                      "row": 1
                    },
                    "idempotency_key": "8e1d0a67-3b45-4c92-9f70-a2d61c5e3840"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WidgetsCreateImageResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/widgets/update-image": {
      "post": {
        "operationId": "updateImageWidget",
        "tags": [
          "Widgets"
        ],
        "summary": "Replace the picture of an image widget",
        "description": "Replaces the picture of an image widget that already exists, with the same rules /v1/widgets/create-image applies to the file: at most 716800 bytes decoded, really a JPEG, a PNG or a GIF, and the new width and height read off the picture.\n\nIT REPLACES THE PICTURE AND NOTHING ELSE. The name, the row, the column and the section of the widget stay as they are: a rename is /v1/widgets/rename and a move is /v1/widgets/move. No cached result is dropped, because an image is never executed against a provider.\n\nTHE PREVIOUS FILE IS NOT KEPT. A backup mirrors the DEFINITION of a widget and an image has none, so the only way back is uploading the previous picture again; the answer carries a `before` saying what the one it replaced measured and weighed.\n\nA report built on a template is refused with 400 APPEND_NOT_ALLOWED, reason template_based: the product makes a copy per derived report of a text block and of an Excel or Sheets widget and of nothing else, so every picture such a report shows belongs to its template.\n\nRefusals: 400 for the framework and the body, 400 INVALID_VALUE, 400 APPEND_NOT_ALLOWED, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 SECTION_BUSY, 409 REPORT_IN_TRASH and 413 PAYLOAD_TOO_LARGE.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.\n\nA file that is refused is 400 INVALID_VALUE with the rule that stopped it in `details.reason`, one of: `too_large` (over 716800 decoded bytes), `too_many_pixels` (the header announces more than 25000000 pixels, which is a picture nothing draws and a decode nothing should pay for), `format_not_supported` (the first bytes are not a JPEG, a PNG or a GIF, whatever the file is called) and `not_an_image` (the bytes announce a format and do not decode as one).",
        "x-mcp-description": "Use the update_image_widget tool to swap the picture of an image widget. The client encodes the file; never generate the bytes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WidgetsUpdateImageRequest"
              },
              "examples": {
                "replaced": {
                  "summary": "A new logo in the widget that already holds one (the base64 is cut short here).",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "section_key": "5b2e8c14a9f04d3786c1e0b7d5493fa2",
                    "widget_key": "7a5d2c910eb34f68b9c07d4e15382fa6",
                    "image_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
                    "filename": "logo-2026.png",
                    "idempotency_key": "0b39f5c8-6e27-4a14-b8d0-53f7e91ca264"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WidgetsUpdateImageResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/sections/reorder": {
      "post": {
        "operationId": "reorderSections",
        "tags": [
          "Sections"
        ],
        "summary": "Reorder the sections of a report",
        "description": "Puts the sections of a report in another order and raises the version of its sections structure.\n\nsection_keys carries the same keys /v1/reports/get publishes for the report, each of them once, in the order they are to be read: a list that adds, drops or repeats a key is refused with 400 INVALID_VALUE instead of being merged onto what is stored, and a list that stopped being an exact permutation between the read and the write is refused with 409 VERSION_MISMATCH inside the transaction that writes -- that one carries the version the report is at and the reason, and no `expected`, because what went stale was the list of sections and not a number the caller sent. Read the report, rearrange the keys it gave you, and send them back.\n\nIt accepts if_version and compares it again inside that transaction. It changes nothing about what any widget shows or reads, so no cached result is dropped.\n\nA report built on a template is refused with 400 APPEND_NOT_ALLOWED, reason template_based: its sections, and their order, belong to its template.\n\nRefusals: 400 for the framework and the body, 400 INVALID_VALUE, 400 APPEND_NOT_ALLOWED, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 VERSION_MISMATCH, 409 SECTION_BUSY and 409 REPORT_IN_TRASH.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the reorder_sections tool to change the order the sections of a report are read in, sending back the keys get_report published.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SectionsReorderRequest"
              },
              "examples": {
                "reordered": {
                  "summary": "The three sections of a report, the last one first.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "section_keys": [
                      "e2b7c5490da34f1789c6b0d5f38241ae",
                      "c6a04e83d71b429fa58d2c9e37016b52",
                      "b1d7f0925ac3482ea6e9c4d81f5307ab"
                    ],
                    "if_version": 7,
                    "idempotency_key": "4a9e6c20-7b31-4d58-a0f2-9e63c1d8b57f"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SectionsReorderResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/sections/create": {
      "post": {
        "operationId": "createSection",
        "tags": [
          "Sections"
        ],
        "summary": "Add a section to a report",
        "description": "Adds one empty section at the END of a report and raises the version of its sections structure.\n\nThe section is born with the defaults of the product -- the date range a new section gets, the flags of its header, its page orientation and every global filter set to none -- which are the same ones every section of a new report is created with. Only `name` and `page_break` are asked for; everything else is changed afterwards with /v1/sections/update.\n\nIT GOES AT THE END and there is no way to ask for another position, which is what the application offers too: move it with /v1/sections/reorder. It starts EMPTY -- widgets go in with /v1/widgets/create, or a whole section with its widgets is written in one call with /v1/reports/append.\n\nThere is no ceiling on the sections of a report. It accepts if_version and answers the new one.\n\nA report built on a template is refused with 400 APPEND_NOT_ALLOWED, reason template_based: its sections belong to its template.\n\nRefusals: 400 for the framework and the body, 400 VALIDATION_FAILED, 400 APPEND_NOT_ALLOWED, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 VERSION_MISMATCH and 409 REPORT_IN_TRASH.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the create_section tool to add an empty section at the end of a report, then create_widget to fill it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SectionsCreateRequest"
              },
              "examples": {
                "named": {
                  "summary": "One more section, with a page break before it.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "name": "Paid search",
                    "page_break": true,
                    "idempotency_key": "2f7b1c40-9d3e-4a85-b6c1-8e0f5a7d2934"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SectionsCreateResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/sections/update": {
      "post": {
        "operationId": "updateSection",
        "tags": [
          "Sections"
        ],
        "summary": "Change the settings of a section",
        "description": "Changes the settings of one section of a report: its title, what its header shows, how its pages are laid out in the PDF, and the period its widgets read.\n\nIT IS PARTIAL. The body carries only the fields it changes and every other column of the section is kept -- including the temporary filter a client portal may be showing the report through, which no partial body names. The answer carries a `before` with the previous value of each field it touched, and sending those values back is what undoes the call.\n\n`show_previous_period` DEPENDS ON `show_date_range`: the period a section is compared against is drawn under the date range of its header, so a section that hides the range cannot show the comparison. A body that asks for both writes show_previous_period false and says so in warnings, which is what the settings window of the application does with its two controls.\n\n`date_range` moves THIS section, and with it every widget of the section that carries no period of its own; the cached results of those widgets are dropped. The date_range of /v1/reports/update is the one that moves every section at once.\n\nThe thirteen global filters of a section, its Analytics segment and the account that segment is read from are NOT written by this version: capabilities lists them under sections_update.not_yet_available.\n\nIt does not touch the list of sections, so it takes no if_version and raises no version. A report built on a template is refused with 400 APPEND_NOT_ALLOWED, reason template_based: its sections belong to its template, where a change reaches every report built on it.\n\nRefusals: 400 for the framework and the body, 400 VALIDATION_FAILED, 400 INVALID_VALUE, 400 APPEND_NOT_ALLOWED, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 SECTION_CHANGED when the settings of the section changed after the call read them, in which case nothing is written and the call can be retried after reading the section again, and 409 REPORT_IN_TRASH.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the update_section tool to rename a section, change what its header shows or move it to another period.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SectionsUpdateRequest"
              },
              "examples": {
                "renamed": {
                  "summary": "Only the title of the section.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "section_key": "5b2e8c14a9f04d3786c1e0b7d5493fa2",
                    "name": "Paid search - September",
                    "idempotency_key": "91c4e2b0-5f76-4d18-9a3c-2e7b0d4f6851"
                  }
                },
                "period": {
                  "summary": "The period this section reads, and the comparison in its header.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "section_key": "5b2e8c14a9f04d3786c1e0b7d5493fa2",
                    "show_date_range": true,
                    "show_previous_period": true,
                    "date_range": {
                      "preset": "lastmonth"
                    },
                    "idempotency_key": "7d0a5f31-2c94-4e6b-85af-13c6e9b02d78"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SectionsUpdateResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/sections/copy": {
      "post": {
        "operationId": "copySection",
        "tags": [
          "Sections"
        ],
        "summary": "Copy a section into another report",
        "description": "Copies one section of a report, with every widget in it, into ANOTHER report of the same account, and raises the version of the target report's sections structure.\n\nTHE COPY IS RE-BOUND TO THE REPORT IT LANDS IN. Every widget of it follows the way the TARGET binds its data sources: in a report with a report level binding each widget reads the target's connection for its integration, and in a report that binds a connection per widget the copies arrive with none and are pointed at one with /v1/widgets/set-connection or /v1/reports/set-connections. That re-binding is the whole reason the source and the target have to be two different reports: run inside one it would clear the connections the widgets already have. To copy a section where it already is, use /v1/sections/duplicate.\n\nThe copy lands at the END of the target report, named after the original with the copy suffix of the account's language (trimmed to the 80 characters a section name takes: the suffix is kept whole and the title gives way), and the client briefs of its AI widgets travel with it.\n\nIT NEVER CROSSES ACCOUNTS: both reports are the caller's own. Neither end can be a report built on a template -- the target has no sections of its own to receive one, and the ones a derived report shows belong to its template (400 APPEND_NOT_ALLOWED, reason template_based).\n\nAN AI SUMMARY INSIDE THE SECTION KEEPS WHAT TRAVELS WITH IT and loses the rest: the widgets copied beside it and the section itself are re-pointed at their copies, and any key that named a section or a widget of the report it came from is dropped, because in the target it names nothing. `lost_selections` enumerates them, so the summary can be pointed at something again.\n\nRefusals: 400 for the framework and the body, 400 INVALID_VALUE, 400 APPEND_NOT_ALLOWED, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 VERSION_MISMATCH and 409 REPORT_IN_TRASH.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the copy_section tool to carry a whole section of analysis into another report instead of describing it again.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SectionsCopyRequest"
              },
              "examples": {
                "imported": {
                  "summary": "A section of last month's report carried into this month's.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "source_report_key": "c41b7f05e8d2436a9017be5c2f83d19a",
                    "section_key": "5b2e8c14a9f04d3786c1e0b7d5493fa2",
                    "idempotency_key": "4a6c9e12-7b35-4f80-a1d9-6e2540bc7f93"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SectionsCopyResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/sections/duplicate": {
      "post": {
        "operationId": "duplicateSection",
        "tags": [
          "Sections"
        ],
        "summary": "Duplicate a section inside its report",
        "description": "Copies one section of a report, with every widget in it, INTO THE SAME REPORT, and raises the version of its sections structure.\n\nIT IS NOT /v1/sections/copy WITH THE SAME REPORT TWICE, and that is why it is a route of its own. A copy that lands in ANOTHER report re-binds every widget to the connections of the target and drops the AI selections that named sections of the report it came from, because there those keys name nothing. Inside one report neither is right: the duplicate reads the same connections the original reads, and a summary in it that named a sibling section still has that sibling beside it. So nothing is re-bound and nothing is dropped -- which is exactly what the duplicate button of the section header does.\n\nThe copy lands at the END of the report, named after the original with the copy suffix of the account's language, trimmed to the 80 characters a section name takes: the suffix is kept whole and the title it came from is what gives way. The client briefs of its AI widgets travel with it.\n\nIt accepts if_version and compares it again inside the transaction that writes. A report built on a template is refused with 400 APPEND_NOT_ALLOWED, reason template_based: its sections belong to its template.\n\nRefusals: 400 for the framework and the body, 400 APPEND_NOT_ALLOWED, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 VERSION_MISMATCH and 409 REPORT_IN_TRASH.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the duplicate_section tool to repeat a section inside its own report, then edit the copy.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SectionsDuplicateRequest"
              },
              "examples": {
                "repeated": {
                  "summary": "One more copy of a section that already works.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "section_key": "5b2e8c14a9f04d3786c1e0b7d5493fa2",
                    "idempotency_key": "6d2f81a3-0e74-4c59-b83a-9f15e7c0d462"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SectionsDuplicateResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/filters/list": {
      "post": {
        "operationId": "listFilters",
        "tags": [
          "Filters"
        ],
        "summary": "List the saved filters",
        "description": "Returns the saved filters of the account, each with the integration and the data view it belongs to and with its conditions in the same shape the filters block of a widget definition takes. A filter whose conditions cannot be named in public is listed with conditions null and the reason why.",
        "x-mcp-description": "Use the list_filters tool to find the filter_key of a filter and to copy its conditions into a widget definition.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FiltersListRequest"
              },
              "examples": {
                "all": {
                  "summary": "Every saved filter of the account.",
                  "value": {}
                },
                "of_one_integration": {
                  "summary": "Only the filters of one integration.",
                  "value": {
                    "integration_id": "google_ads"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FiltersListResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "One filter of Google Ads.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "filters": [
                          {
                            "filter_key": "3a19f7c8d04b4e6690b1c25e87f3a4d1",
                            "name": "Brand campaigns",
                            "display_name": "Brand",
                            "internal_notes": "Everything whose campaign name starts with BR_",
                            "integration_id": "google_ads",
                            "level": "campaign",
                            "is_system_defined": false,
                            "conditions": {
                              "combination": "and",
                              "conditions": [
                                {
                                  "field": "campaign.name",
                                  "operator": "starts_with",
                                  "value": "BR_",
                                  "type_of_field": null
                                }
                              ]
                            },
                            "not_expressible_reason": null,
                            "not_expressible_message": null
                          }
                        ],
                        "count": 1
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-21T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/filters/get": {
      "post": {
        "operationId": "getFilter",
        "tags": [
          "Filters"
        ],
        "summary": "Read one saved filter",
        "description": "Returns one saved filter by key, in the same shape the listing publishes it. Which reports and widgets use it is not published.",
        "x-mcp-description": "Use the get_filter tool to read the conditions of a filter a widget or a report points at.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FilterGetRequest"
              },
              "examples": {
                "get": {
                  "summary": "One filter by key.",
                  "value": {
                    "filter_key": "3a19f7c8d04b4e6690b1c25e87f3a4d1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FilterGetResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The filter.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "filter": {
                          "filter_key": "3a19f7c8d04b4e6690b1c25e87f3a4d1",
                          "name": "Brand campaigns",
                          "display_name": "Brand",
                          "internal_notes": "Everything whose campaign name starts with BR_",
                          "integration_id": "google_ads",
                          "level": "campaign",
                          "is_system_defined": false,
                          "conditions": {
                            "combination": "and",
                            "conditions": [
                              {
                                "field": "campaign.name",
                                "operator": "starts_with",
                                "value": "BR_",
                                "type_of_field": null
                              }
                            ]
                          },
                          "not_expressible_reason": null,
                          "not_expressible_message": null
                        }
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-21T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/calculated-metrics/list": {
      "post": {
        "operationId": "listCalculatedMetrics",
        "tags": [
          "Calculated Metrics"
        ],
        "summary": "List the calculated metrics",
        "description": "Returns the calculated metrics of the account, each with the integration and the data view it belongs to and with the block of its kind: a formula, a metric aggregated over a period, or a metric read through a saved filter. A calculated metric can be read with /v1/fields and asked for in /v1/query; the definition of a widget does not take one.",
        "x-mcp-description": "Use the list_calculated_metrics tool to see which metrics the account defined itself and to read or query them. A widget definition does not take a calculated metric.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CalculatedMetricsListRequest"
              },
              "examples": {
                "all": {
                  "summary": "Every calculated metric of the account.",
                  "value": {}
                },
                "of_one_integration": {
                  "summary": "Only the metrics of one integration.",
                  "value": {
                    "integration_id": "google_ads"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CalculatedMetricsListResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "One metric written as a formula.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "calculated_metrics": [
                          {
                            "calculated_metric_key": "b7d2e64f3c1a7e429f0596ce18a3b7d2",
                            "name": "ROAS",
                            "display_name": "ROAS",
                            "internal_notes": null,
                            "integration_id": "google_ads",
                            "level": "campaign",
                            "type": "expression",
                            "is_system_defined": true,
                            "expression": {
                              "format": "none",
                              "formula": [
                                "conversions_value",
                                "/",
                                "cost"
                              ],
                              "green_when_increases": true
                            },
                            "function": null,
                            "filtered": null,
                            "not_expressible_reason": null,
                            "not_expressible_message": null
                          }
                        ],
                        "count": 1
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-21T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/calculated-metrics/get": {
      "post": {
        "operationId": "getCalculatedMetric",
        "tags": [
          "Calculated Metrics"
        ],
        "summary": "Read one calculated metric",
        "description": "Returns one calculated metric by key, in the same shape the listing publishes it. Which reports and widgets use it is not published.",
        "x-mcp-description": "Use the get_calculated_metric tool to read the definition of a metric a report of the account uses.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CalculatedMetricGetRequest"
              },
              "examples": {
                "get": {
                  "summary": "One metric by key.",
                  "value": {
                    "calculated_metric_key": "b7d2e64f3c1a7e429f0596ce18a3b7d2"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CalculatedMetricGetResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The metric.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "calculated_metric": {
                          "calculated_metric_key": "b7d2e64f3c1a7e429f0596ce18a3b7d2",
                          "name": "ROAS",
                          "display_name": "ROAS",
                          "internal_notes": null,
                          "integration_id": "google_ads",
                          "level": "campaign",
                          "type": "expression",
                          "is_system_defined": true,
                          "expression": {
                            "format": "none",
                            "formula": [
                              "conversions_value",
                              "/",
                              "cost"
                            ],
                            "green_when_increases": true
                          },
                          "function": null,
                          "filtered": null,
                          "not_expressible_reason": null,
                          "not_expressible_message": null
                        }
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-21T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/brand-templates/list": {
      "post": {
        "operationId": "listBrandTemplates",
        "tags": [
          "Brand Templates"
        ],
        "summary": "List the brand templates",
        "description": "Returns the brands a report can be printed with: the one of the platform, under the empty key, and the ones of the account. The logo and the cover image are reported as present or absent and never carried.",
        "x-mcp-description": "Use the list_brand_templates tool to find the brand_template_key that update_report takes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BrandTemplatesListRequest"
              },
              "examples": {
                "list": {
                  "summary": "The endpoint reads no parameters.",
                  "value": {}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BrandTemplatesListResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The brand of the platform and one of the account.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "brand_templates": [
                          {
                            "brand_template_key": "",
                            "name": "System defined brand template",
                            "internal_notes": "This brand template can be copied but it can not be changed or removed",
                            "is_system": true,
                            "has_logo": true,
                            "has_cover_image": false,
                            "primary_color": "#2f5fdd",
                            "font_family": "Arial"
                          },
                          {
                            "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                            "name": "Acme",
                            "internal_notes": null,
                            "is_system": false,
                            "has_logo": true,
                            "has_cover_image": true,
                            "primary_color": "#0b5f3a",
                            "font_family": "Open Sans"
                          }
                        ],
                        "count": 2
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-21T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/brand-templates/get": {
      "post": {
        "operationId": "getBrandTemplate",
        "tags": [
          "Brand Templates"
        ],
        "summary": "Read one brand template",
        "description": "Returns every setting of one brand template: margins, the colour and the font of each part of the report, the palette of the charts, the footer, the front cover, the table of contents and the header of the client portal.",
        "x-mcp-description": "Use the get_brand_template tool to see how a report printed with a brand actually looks.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BrandTemplateGetRequest"
              },
              "examples": {
                "get": {
                  "summary": "One brand template by key.",
                  "value": {
                    "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96"
                  }
                },
                "system": {
                  "summary": "The brand of the platform.",
                  "value": {
                    "brand_template_key": ""
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BrandTemplateGetResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "One brand template of the account.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "brand_template": {
                          "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                          "is_system": false,
                          "name": "Acme",
                          "internal_notes": null,
                          "font_family": "Open Sans",
                          "margin_top": 15,
                          "margin_bottom": 15,
                          "margin_left": 20,
                          "margin_right": 20,
                          "report_title_text_color": "#000000",
                          "report_title_font_size": 24,
                          "report_title_font_weight": "bold",
                          "report_subtitle_text_color": "#000000",
                          "report_subtitle_font_size": 20,
                          "report_subtitle_font_weight": "bold",
                          "section_name_text_color": "#000000",
                          "section_name_font_size": 14,
                          "section_name_font_weight": "bold",
                          "date_range_text_color": "#000000",
                          "date_range_font_size": 11,
                          "date_range_font_weight": "normal",
                          "component_name_text_color": "#000000",
                          "component_name_font_size": 9,
                          "component_name_font_weight": "bold",
                          "footer_text_color": "#000000",
                          "footer_font_size": 7,
                          "footer_font_weight": "normal",
                          "include_logo": true,
                          "include_section_line": true,
                          "section_line_color": "#000000",
                          "component_name_background_color": "#ffffff",
                          "component_content_text_color": "#000000",
                          "component_content_font_size": 8,
                          "colors": [
                            "#4684ee",
                            "#dc3912",
                            "#ff9900"
                          ],
                          "footer_content": "",
                          "include_footer_line": false,
                          "footer_line_color": "#000000",
                          "primary_color": "#0b5f3a",
                          "cover_style": "gradient",
                          "cover_tone": "auto",
                          "cover_text_color": null,
                          "cover_inverted": false,
                          "prepared_by": "",
                          "prepared_for": "",
                          "toc_style": "cards",
                          "portal_header_style": "none",
                          "portal_header_logo": true,
                          "web_font_family": "app_default",
                          "logo": {
                            "present": true
                          },
                          "cover_image": {
                            "present": true
                          }
                        }
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-21T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/brand-templates/create": {
      "post": {
        "operationId": "createBrandTemplate",
        "tags": [
          "Brand Templates"
        ],
        "summary": "Create a brand template",
        "description": "Creates a brand template of the account: the look a report is printed with and a client portal view is drawn with -- the fonts, the colours, the margins, the front cover, the table of contents and the header of the portal.\n\nA NEW TEMPLATE IS BORN COMPLETE. Every setting a body does not carry takes the default of the product, and the template is born with the logo of the platform, which is what makes include_logo true mean something on the first call. A body with a name alone therefore writes a template that prints correctly; send only what is meant to differ.\n\nTHE BODY IS THE OBJECT /v1/brand-templates/get ANSWERS. The keys that read publishes and a write has no column for -- brand_template_key, is_system, logo and cover_image -- are accepted and ignored with a READ_ONLY_FIELD_IGNORED warning, and a null means \"leave it as it is\", which on a creation means the default.\n\nTHE TWO IMAGES ARE NOT WRITTEN HERE: /v1/brand-templates/set-logo and /v1/brand-templates/set-cover-image each write one file. A creation with cover_style 'image' is refused with details.reason cover_image_required, because a template being created carries no image yet.\n\nColours are written as #RRGGBB and stored in lower case. cover_tone auto, web_font_family app_default and cover_inverted false are stored as an absent column, which is what the settings window of the application writes for them, and a read publishes the same values back. colors does not change size.\n\nif_version is not accepted, and the call is never confirmed: its inverse is /v1/brand-templates/delete.\n\nRefusals: 400 for the framework and the body (VALIDATION_FAILED, one entry per field), 403 WRITE_ACCESS_DISABLED.\n\nA brand template written through this API is not visible to an application window that is already open: the page has to be reloaded before the same template is edited there.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the create_brand_template tool.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BrandTemplatesCreateRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "name": "Acme, dark",
                    "primary_color": "#1f3b73",
                    "cover_style": "gradient",
                    "font_family": "Inter",
                    "idempotency_key": "acme-dark-brand-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BrandTemplatesCreateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The brand template as it is after the call.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "brand_template": {
                          "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                          "is_system": false,
                          "name": "Acme, dark",
                          "internal_notes": null,
                          "font_family": "Inter",
                          "margin_top": 15,
                          "margin_bottom": 15,
                          "margin_left": 20,
                          "margin_right": 20,
                          "report_title_text_color": "#000000",
                          "report_title_font_size": 24,
                          "report_title_font_weight": "bold",
                          "report_subtitle_text_color": "#000000",
                          "report_subtitle_font_size": 20,
                          "report_subtitle_font_weight": "bold",
                          "section_name_text_color": "#000000",
                          "section_name_font_size": 14,
                          "section_name_font_weight": "bold",
                          "date_range_text_color": "#000000",
                          "date_range_font_size": 11,
                          "date_range_font_weight": "normal",
                          "component_name_text_color": "#000000",
                          "component_name_font_size": 9,
                          "component_name_font_weight": "bold",
                          "footer_text_color": "#000000",
                          "footer_font_size": 7,
                          "footer_font_weight": "normal",
                          "include_logo": true,
                          "include_section_line": true,
                          "section_line_color": "#000000",
                          "component_name_background_color": "#ffffff",
                          "component_content_text_color": "#000000",
                          "component_content_font_size": 8,
                          "colors": [
                            "#4684ee",
                            "#dc3912",
                            "#ff9900"
                          ],
                          "footer_content": "",
                          "include_footer_line": false,
                          "footer_line_color": "#000000",
                          "primary_color": "#1f3b73",
                          "cover_style": "gradient",
                          "cover_tone": "auto",
                          "cover_text_color": null,
                          "cover_inverted": false,
                          "prepared_by": "",
                          "prepared_for": "",
                          "toc_style": "cards",
                          "portal_header_style": "none",
                          "portal_header_logo": true,
                          "web_font_family": "app_default",
                          "logo": {
                            "present": true
                          },
                          "cover_image": {
                            "present": true
                          }
                        },
                        "created_keys": [
                          "a4f71c085d3b4e29b6c0a83f251d7e96"
                        ],
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-22T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/brand-templates/update": {
      "post": {
        "operationId": "updateBrandTemplate",
        "tags": [
          "Brand Templates"
        ],
        "summary": "Update a brand template",
        "description": "Changes the settings of a brand template that already exists.\n\nIT IS SHARED. Every report printed with the template and every client portal view wearing it changes with it, from the next time they are drawn. /v1/brand-templates/delete names them when it refuses to delete one.\n\nTHE UPDATE IS PARTIAL: the fields the body carries are written and every other column is left as it is, the logo and the cover image included. The answer carries `before` with the whole template in the same shape, so sending that block back undoes the call.\n\nTWO PAIRS THE SETTINGS WINDOW OF THE APPLICATION KEEPS AND THIS ENDPOINT KEEPS WITH IT. include_logo true over a template that carries no logo is refused with 400 and details.reason logo_required; cover_style 'image' over one that carries no cover image is refused with details.reason cover_image_required.\n\nSETTING include_logo FALSE REMOVES THE LOGO. The flag and the file are one thing: unticking the checkbox in the settings window of the application blanks the file with it, and this endpoint does the same, in the same write. Nothing keeps a copy, so switching the flag on again needs a new upload through /v1/brand-templates/set-logo; the answer says so with a LOGO_REMOVED warning, and a read afterwards answers logo null. The other way to remove it is /v1/brand-templates/set-logo with a null file, which is refused while the flag is on.\n\nA WRITE OF SETTINGS NEVER WRITES THE BYTES OF AN IMAGE. It writes the columns that are settings and leaves the logo and the cover image where they are, so an upload running at the same time is never undone by it; the one thing that reaches the logo column is the removal above, and it only ever writes an absence.\n\nThe brand of the platform, under the empty key, answers 409 NOT_ALLOWED with details.reason system_defined.\n\nif_version is not accepted.\n\nRefusals: 400 for the framework and the body, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND when the template is not this account's, 409 NOT_ALLOWED.\n\nA brand template written through this API is not visible to an application window that is already open: the page has to be reloaded before the same template is edited there.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the update_brand_template tool.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BrandTemplatesUpdateRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                    "primary_color": "#0b5d1e",
                    "idempotency_key": "acme-brand-colour-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BrandTemplatesUpdateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The brand template as it is after the call.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "brand_template": {
                          "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                          "is_system": false,
                          "name": "Acme",
                          "internal_notes": null,
                          "font_family": "Open Sans",
                          "margin_top": 15,
                          "margin_bottom": 15,
                          "margin_left": 20,
                          "margin_right": 20,
                          "report_title_text_color": "#000000",
                          "report_title_font_size": 24,
                          "report_title_font_weight": "bold",
                          "report_subtitle_text_color": "#000000",
                          "report_subtitle_font_size": 20,
                          "report_subtitle_font_weight": "bold",
                          "section_name_text_color": "#000000",
                          "section_name_font_size": 14,
                          "section_name_font_weight": "bold",
                          "date_range_text_color": "#000000",
                          "date_range_font_size": 11,
                          "date_range_font_weight": "normal",
                          "component_name_text_color": "#000000",
                          "component_name_font_size": 9,
                          "component_name_font_weight": "bold",
                          "footer_text_color": "#000000",
                          "footer_font_size": 7,
                          "footer_font_weight": "normal",
                          "include_logo": true,
                          "include_section_line": true,
                          "section_line_color": "#000000",
                          "component_name_background_color": "#ffffff",
                          "component_content_text_color": "#000000",
                          "component_content_font_size": 8,
                          "colors": [
                            "#4684ee",
                            "#dc3912",
                            "#ff9900"
                          ],
                          "footer_content": "",
                          "include_footer_line": false,
                          "footer_line_color": "#000000",
                          "primary_color": "#0b5d1e",
                          "cover_style": "gradient",
                          "cover_tone": "auto",
                          "cover_text_color": null,
                          "cover_inverted": false,
                          "prepared_by": "",
                          "prepared_for": "",
                          "toc_style": "cards",
                          "portal_header_style": "none",
                          "portal_header_logo": true,
                          "web_font_family": "app_default",
                          "logo": {
                            "present": true
                          },
                          "cover_image": {
                            "present": true
                          }
                        },
                        "before": {
                          "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                          "is_system": false,
                          "name": "Acme",
                          "internal_notes": null,
                          "font_family": "Open Sans",
                          "margin_top": 15,
                          "margin_bottom": 15,
                          "margin_left": 20,
                          "margin_right": 20,
                          "report_title_text_color": "#000000",
                          "report_title_font_size": 24,
                          "report_title_font_weight": "bold",
                          "report_subtitle_text_color": "#000000",
                          "report_subtitle_font_size": 20,
                          "report_subtitle_font_weight": "bold",
                          "section_name_text_color": "#000000",
                          "section_name_font_size": 14,
                          "section_name_font_weight": "bold",
                          "date_range_text_color": "#000000",
                          "date_range_font_size": 11,
                          "date_range_font_weight": "normal",
                          "component_name_text_color": "#000000",
                          "component_name_font_size": 9,
                          "component_name_font_weight": "bold",
                          "footer_text_color": "#000000",
                          "footer_font_size": 7,
                          "footer_font_weight": "normal",
                          "include_logo": true,
                          "include_section_line": true,
                          "section_line_color": "#000000",
                          "component_name_background_color": "#ffffff",
                          "component_content_text_color": "#000000",
                          "component_content_font_size": 8,
                          "colors": [
                            "#4684ee",
                            "#dc3912",
                            "#ff9900"
                          ],
                          "footer_content": "",
                          "include_footer_line": false,
                          "footer_line_color": "#000000",
                          "primary_color": "#0b5f3a",
                          "cover_style": "gradient",
                          "cover_tone": "auto",
                          "cover_text_color": null,
                          "cover_inverted": false,
                          "prepared_by": "",
                          "prepared_for": "",
                          "toc_style": "cards",
                          "portal_header_style": "none",
                          "portal_header_logo": true,
                          "web_font_family": "app_default",
                          "logo": {
                            "present": true
                          },
                          "cover_image": {
                            "present": true
                          }
                        },
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-22T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/brand-templates/delete": {
      "post": {
        "operationId": "deleteBrandTemplate",
        "tags": [
          "Brand Templates"
        ],
        "summary": "Delete a brand template",
        "description": "Deletes a brand template of the account.\n\nTHERE IS NO WAY BACK, AND THE TWO IMAGES GO WITH THE ROW. /v1/brand-templates/create writes a NEW template, with a new key; the logo and the cover image would have to be uploaded again, and every report pointed at the deleted one would have to be pointed at it. That is why this endpoint ALWAYS asks for a confirmation and why its answer carries `before` with every setting, which is what it would take to write it again.\n\nIT REFUSES WHILE ANYTHING STILL USES IT: 409 IN_USE, with details.used_by naming what -- `reports`, each with its report_key, its internal name and whether it is in the trash, and `portal_views`, each with its portal_view_key and its name. Reports in the trash count, like everywhere else in this API: one of them still points at the template and would come back pointing at nothing.\n\nThe account setting that names the brand template new reports are born with is cleared with the row, which is what the application does too.\n\nThe brand of the platform, under the empty key, answers 409 NOT_ALLOWED with details.reason system_defined.\n\nif_version is not accepted.\n\nRefusals: 400 for the framework and the body, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND when the template is not this account's, 409 IN_USE and 409 NOT_ALLOWED.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the delete_brand_template tool. It is refused while any report or portal view still uses the template.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BrandTemplatesDeleteRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                    "idempotency_key": "drop-acme-brand-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BrandTemplatesDeleteResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The first call answers a token; the second is this same body with confirmation_token added.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                        "before": {
                          "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                          "is_system": false,
                          "name": "Acme",
                          "internal_notes": null,
                          "font_family": "Open Sans",
                          "margin_top": 15,
                          "margin_bottom": 15,
                          "margin_left": 20,
                          "margin_right": 20,
                          "report_title_text_color": "#000000",
                          "report_title_font_size": 24,
                          "report_title_font_weight": "bold",
                          "report_subtitle_text_color": "#000000",
                          "report_subtitle_font_size": 20,
                          "report_subtitle_font_weight": "bold",
                          "section_name_text_color": "#000000",
                          "section_name_font_size": 14,
                          "section_name_font_weight": "bold",
                          "date_range_text_color": "#000000",
                          "date_range_font_size": 11,
                          "date_range_font_weight": "normal",
                          "component_name_text_color": "#000000",
                          "component_name_font_size": 9,
                          "component_name_font_weight": "bold",
                          "footer_text_color": "#000000",
                          "footer_font_size": 7,
                          "footer_font_weight": "normal",
                          "include_logo": true,
                          "include_section_line": true,
                          "section_line_color": "#000000",
                          "component_name_background_color": "#ffffff",
                          "component_content_text_color": "#000000",
                          "component_content_font_size": 8,
                          "colors": [
                            "#4684ee",
                            "#dc3912",
                            "#ff9900"
                          ],
                          "footer_content": "",
                          "include_footer_line": false,
                          "footer_line_color": "#000000",
                          "primary_color": "#0b5f3a",
                          "cover_style": "gradient",
                          "cover_tone": "auto",
                          "cover_text_color": null,
                          "cover_inverted": false,
                          "prepared_by": "",
                          "prepared_for": "",
                          "toc_style": "cards",
                          "portal_header_style": "none",
                          "portal_header_logo": true,
                          "web_font_family": "app_default",
                          "logo": {
                            "present": true
                          },
                          "cover_image": {
                            "present": true
                          }
                        },
                        "unfulfilled": [],
                        "warnings": [],
                        "deleted": true
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-22T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/brand-templates/set-logo": {
      "post": {
        "operationId": "setBrandTemplateLogo",
        "tags": [
          "Brand Templates"
        ],
        "summary": "Set the logo of a brand template",
        "description": "Uploads the logo a brand template prints, or removes the one it carries.\n\nTHE FILE TRAVELS BASE64 ENCODED and is at most 153600 decoded bytes and 25000000 pixels. It has to really be a JPEG, a PNG or a GIF: the first bytes decide, not the name, which is read only so a caller that sent the wrong file hears which one. The bytes are stored exactly as they arrive.\n\nREMOVING IT (file null) IS REFUSED WHILE include_logo IS TRUE, with 400 and details.reason logo_required: a template that prints its logo has to carry one, which is what the settings window of the application demands. Set include_logo false with /v1/brand-templates/update first.\n\nTHE PREVIOUS FILE IS NOT KEPT ANYWHERE, so the way back from a replacement is uploading it again; `before` says only whether there was one.\n\nThe brand of the platform, under the empty key, answers 409 NOT_ALLOWED with details.reason system_defined.\n\nif_version is not accepted, and the call is never confirmed.\n\nRefusals: 400 for the framework, the body and the file (details.reason is one of too_large, too_many_pixels, format_not_supported, not_an_image, logo_required), 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND when the template is not this account's, 409 NOT_ALLOWED, 413 when the whole body is over the request ceiling.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the set_brand_template_logo tool.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BrandTemplatesSetLogoRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                    "file": {
                      "name": "acme-logo.png",
                      "content_base64": "iVBORw0KGgoAAAANSUhEUgAA..."
                    },
                    "idempotency_key": "acme-brand-logo-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BrandTemplatesSetLogoResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The brand template as it is after the call.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                        "logo": {
                          "present": true
                        },
                        "before": {
                          "logo": {
                            "present": true
                          }
                        },
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-22T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/brand-templates/set-cover-image": {
      "post": {
        "operationId": "setBrandTemplateCoverImage",
        "tags": [
          "Brand Templates"
        ],
        "summary": "Set the cover image of a brand template",
        "description": "Uploads the photograph the 'image' front cover of a brand template draws, or removes the one it carries.\n\nTHE FILE TRAVELS BASE64 ENCODED and is at most 716800 decoded bytes and 25000000 pixels, and it is JPEG OR PNG ONLY: a GIF is refused with details.reason format_not_supported, which is what the upload of the application accepts too. That refusal has ONE shape whichever half of the body gives the file away -- a name ending in .gif is refused on the spot with the same reason its bytes would have got -- and for everything else the first bytes decide, never the name.\n\nTHE FILE IS NORMALISED WHERE POSSIBLE. What is stored is a copy scaled so its longest side is at most 1600 pixels and aiming at 409600 bytes. It is not a guarantee of a size: a file that cannot be decoded, and one whose copy comes out no smaller, is stored exactly as it was sent.\n\nUPLOADING DOES NOT CHANGE THE COVER STYLE: set cover_style 'image' with /v1/brand-templates/update afterwards. Removing the image (file null) is refused with 400 and details.reason cover_image_required while the style is 'image', because that cover would have nothing to draw.\n\nTHE PREVIOUS FILE IS NOT KEPT ANYWHERE, so the way back from a replacement is uploading it again; `before` says only whether there was one.\n\nThe brand of the platform, under the empty key, answers 409 NOT_ALLOWED with details.reason system_defined.\n\nif_version is not accepted, and the call is never confirmed.\n\nRefusals: 400 for the framework, the body and the file (details.reason is one of too_large, too_many_pixels, format_not_supported, not_an_image, cover_image_required), 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND when the template is not this account's, 409 NOT_ALLOWED, 413 when the whole body is over the request ceiling.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the set_brand_template_cover_image tool.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BrandTemplatesSetCoverImageRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                    "file": {
                      "name": "acme-cover.jpg",
                      "content_base64": "/9j/4AAQSkZJRgABAQEAYABg..."
                    },
                    "idempotency_key": "acme-brand-cover-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BrandTemplatesSetCoverImageResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The brand template as it is after the call.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "brand_template_key": "a4f71c085d3b4e29b6c0a83f251d7e96",
                        "cover_image": {
                          "present": true
                        },
                        "before": {
                          "cover_image": null
                        },
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-22T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/language-files/list": {
      "post": {
        "operationId": "listLanguageFiles",
        "tags": [
          "Language Files"
        ],
        "summary": "List the language files",
        "description": "Returns the language files a report can be printed with: the sixteen of the platform and the ones the account made itself. The literals of a file are not published.",
        "x-mcp-description": "Use the list_language_files tool to find the language_file key that update_report takes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LanguageFilesListRequest"
              },
              "examples": {
                "list": {
                  "summary": "The endpoint reads no parameters.",
                  "value": {}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LanguageFilesListResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "One file of the platform and one of the account.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "language_files": [
                          {
                            "language_file_key": "language_file_en",
                            "name": "English",
                            "language": "en",
                            "is_system": true,
                            "file_name": null,
                            "internal_notes": "This language file can't be copied, changed or removed, but you can download it and create a new one based on it"
                          },
                          {
                            "language_file_key": "c25e87f3a4d13a19f7c8d04b4e6690b1",
                            "name": "Acme wording",
                            "language": "en",
                            "is_system": false,
                            "file_name": "acme-wording.txt",
                            "internal_notes": null
                          }
                        ],
                        "count": 2
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-21T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/email-templates/list": {
      "post": {
        "operationId": "listEmailTemplates",
        "tags": [
          "Email Templates"
        ],
        "summary": "List the email templates",
        "description": "Returns the email templates of the account: the name, the format of the body, the sender, the reply-to address and the subject of each one. The body itself is not published, and the system template is not listed: /v1/schedules/list says which deliveries use it.",
        "x-mcp-description": "Use the list_email_templates tool to find the email_template_key that create_schedule and update_schedule take.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailTemplatesListRequest"
              },
              "examples": {
                "list": {
                  "summary": "The endpoint reads no parameters.",
                  "value": {}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailTemplatesListResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "One template of the account.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "email_templates": [
                          {
                            "email_template_key": "7e03c2a951f6875c94a7e208f34b16bd",
                            "name": "Monthly delivery",
                            "internal_notes": null,
                            "format": "html",
                            "sender": "Acme Reporting",
                            "sender_email": "reports@acme.com",
                            "reply_to_email": "hello@acme.com",
                            "subject": "Your %%REPORT_TITLE%% for %%CURRENT_PERIOD%%"
                          }
                        ],
                        "count": 1
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-21T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/reports/take-snapshot": {
      "post": {
        "operationId": "takeReportSnapshot",
        "tags": [
          "Snapshots"
        ],
        "summary": "Ask for a snapshot of a report",
        "description": "Asks the platform to file a snapshot of a report: the PDF of it as it is right now, kept with the report and listed by /v1/snapshots/list.\n\nTHE FILE IS NOT PRODUCED BY THIS CALL. What the call writes is the request -- a one-off delivery the platform picks up on its next pass -- and the answer is that request, with `status` queued and the `schedule_key` of the row. The snapshot appears in /v1/snapshots/list minutes later, when the report has been generated; there is no endpoint that reports its progress. While it has not been taken, /v1/schedules/delete with that key cancels it.\n\nIT SPENDS AI CREDITS. The report is generated in full, so every AI summary in it is written again and billed exactly as a scheduled delivery would be. `ai_summaries` says how many the report carries, both in the preview and in the answer, and a SPENDS_AI_CREDITS warning accompanies it when there is at least one. It sends no email to anybody, so it does not count against the daily budget of calls that do.\n\nIT IS NOT QUEUED TWICE. A report that already has a snapshot waiting answers 409 ALREADY_QUEUED with `details.schedule_key`: asking twice would generate the report twice and spend the credits twice. A call you are not sure went through is retried with the SAME idempotency_key -- which is also the only thing that makes the guard exact: the check and the write are two steps, so two confirmed calls landing in the same instant with DIFFERENT idempotency keys can still both go through.\n\nThe format is always PDF, and a report keeps its 13 newest snapshots: filing a new one deletes the oldest. Every kind of report is accepted, templates included, exactly as the settings window of the application offers the button on all of them. This endpoint ALWAYS asks for a confirmation. if_version is not accepted.\n\nRefusals: 400 for the framework and the body, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 REPORT_IN_TRASH and 409 ALREADY_QUEUED (`details` = `{schedule_key, report_key}`).\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the take_snapshot tool. The answer is a queued request, not a file: look for the snapshot with list_snapshots afterwards, and never ask twice.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportsTakeSnapshotRequest"
              },
              "examples": {
                "confirm_then_take": {
                  "summary": "The first call answers a token; the second is this same body with confirmation_token added.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                    "idempotency_key": "snapshot-acme-september-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportsTakeSnapshotResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The second call, with the token. The request is queued.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "schedule_key": "7c1de0a4b9f34e2ab58d6c0913fe4287",
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "report_name": "Acme - Monthly performance",
                        "status": "queued",
                        "ai_summaries": 2,
                        "format": "pdf",
                        "unfulfilled": [],
                        "warnings": [
                          {
                            "path": "report_key",
                            "code": "SPENDS_AI_CREDITS",
                            "message": "this report carries 2 AI summary widget(s) and a snapshot generates the report in full: every one of them is written again and spends AI credits of the account, exactly as a scheduled delivery does"
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-21T10:30:00Z"
                      }
                    }
                  },
                  "confirmation_required": {
                    "summary": "The first call: what it would cost, and the token to apply it.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "confirmation_required": true,
                        "confirmation_token": "8f4a1c7e9b2d4f6a8c0e2b4d6f8a0c2e",
                        "expires_at": "2026-09-21T10:40:00Z",
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "report_name": "Acme - Monthly performance",
                        "ai_summaries": 2,
                        "format": "pdf",
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f7",
                        "timestamp": "2026-09-21T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/snapshots/list": {
      "post": {
        "operationId": "listSnapshots",
        "tags": [
          "Snapshots"
        ],
        "summary": "List the snapshots of a report",
        "description": "Returns the snapshots of a report, newest first: what each one is, whether it has a file and what that file is called.\n\nEVERY SNAPSHOT THAT HAS A FILE COMES WITH A SIGNED LINK. `download_url` opens the file with no Authorization header at all, which is what lets an assistant hand it to a person, and it stops working 900 seconds after this call (`download_url_expires_at`). It is null when the snapshot has no file, and a client that finds it null for one that does falls back to `snapshot_key` with the Bearer. A link is good for that one snapshot and cannot be revoked before it expires; list again for a fresh one. The file itself is served by GET /v1/snapshots/download.\n\nThe size of a file is NOT published: measuring it would mean loading it, and a listing of thirteen files is the one place that must not. The download answers Content-Length.",
        "x-mcp-description": "Use the list_snapshots tool, and give the download_url it publishes to the user: it is a direct link that expires in 15 minutes. There is no download tool.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SnapshotsListRequest"
              },
              "examples": {
                "list": {
                  "summary": "The snapshots of one report.",
                  "value": {
                    "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SnapshotsListResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "One snapshot with a file and one whose execution failed.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "snapshots": [
                          {
                            "snapshot_key": "b71c4e9a2d8f45c3a6e0b95d31f7028c",
                            "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                            "type": "manual",
                            "status": "success",
                            "filename": "Acme_-_Monthly_performance.pdf",
                            "created_at": "2026-09-21T06:12:40Z",
                            "download_url": "https://api.reportingninja.com/v1/snapshots/download?token=pT3xK9mQ2fB7yR1sD4hL6vN8cJ0aZ5wE3gU7iO2pY4tX6kM1bV",
                            "download_url_expires_at": "2026-09-21T10:45:00Z"
                          },
                          {
                            "snapshot_key": "3ad90f1c75b24e8fa1c6d02b94e7583f",
                            "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                            "type": "scheduled",
                            "status": "error",
                            "filename": "",
                            "created_at": "2026-09-14T06:00:11Z",
                            "download_url": null,
                            "download_url_expires_at": null
                          }
                        ],
                        "count": 2
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-21T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/snapshots/delete": {
      "post": {
        "operationId": "deleteSnapshot",
        "tags": [
          "Snapshots"
        ],
        "summary": "Delete a snapshot",
        "description": "Deletes one snapshot of a report, with its file.\n\nTHERE IS NO WAY BACK. Nothing keeps a copy of the file, so the only way to have one again is taking another snapshot, which generates the report as it is today and not as it was. That is why this endpoint ALWAYS asks for a confirmation, and why its answer carries `before` with what the snapshot was. A link minted for it by /v1/snapshots/list stops working with a 404.\n\nif_version is not accepted.\n\nRefusals: 400 for the framework and the body, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND when the snapshot is not this account's or is not there any more -- the retention of the platform prunes the oldest while a confirmation is in flight -- and 409 REPORT_IN_TRASH when the report it belongs to is in the trash, the same guard /v1/snapshots/list runs.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the delete_snapshot tool. The file is gone for good.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SnapshotsDeleteRequest"
              },
              "examples": {
                "confirm_then_delete": {
                  "summary": "The first call answers a token; the second is this same body with confirmation_token added.",
                  "value": {
                    "snapshot_key": "b71c4e9a2d8f45c3a6e0b95d31f7028c",
                    "idempotency_key": "drop-acme-august-snapshot-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SnapshotsDeleteResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The second call, with the token. The file is gone.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "snapshot_key": "b71c4e9a2d8f45c3a6e0b95d31f7028c",
                        "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
                        "before": {
                          "type": "manual",
                          "status": "success",
                          "filename": "Acme_-_Monthly_performance.pdf",
                          "created_at": "2026-09-21T06:12:40Z"
                        },
                        "unfulfilled": [],
                        "warnings": [],
                        "deleted": true
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-21T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/snapshots/download": {
      "get": {
        "operationId": "downloadSnapshot",
        "tags": [
          "Snapshots"
        ],
        "summary": "Download the file of a snapshot",
        "description": "Returns the file of a snapshot. It is one of the TWO endpoints of this API that are a GET, whose success is not a JSON envelope and that have no MCP tool of their own; the other one is GET /v1/language-files/download.\n\nTWO WAYS IN, AND ONE OF THEM IS ENOUGH (a request that carries both is served by the token). `token` is the signed link /v1/snapshots/list publishes as `download_url`: it carries the account and the snapshot inside it, needs NO Authorization header -- which is what lets an assistant hand the link to a person -- and expires 900 seconds after it was minted. `snapshot_key` is the form a script uses, with the Bearer of the account; it is charged against the read rate limit like any other call.\n\nThe response is the file itself, `application/pdf` or `application/vnd.ms-excel` according to the name it was stored under, with a Content-Disposition attachment whose filename is that name reduced to ASCII. The signed form has a budget of its own, much narrower than the read rate limit: a link is good for a handful of downloads a minute, which is what handing a file to somebody takes and not what mirroring an account takes. Everything that is not a file comes back in the envelope of the rest of the API: 400 when neither `token` nor `snapshot_key` was sent, 401 with no credentials, 404 when the link expired, the snapshot is gone, it belongs to another account or its execution filed no file, and 429 over the rate limit. A POST to this path is ENDPOINT_NOT_FOUND, and a HEAD is 405.",
        "x-mcp-description": "There is no tool for this endpoint: list_snapshots publishes the signed link and the user opens it.",
        "security": [
          {
            "bearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": false,
            "description": "The signed link of one snapshot, as /v1/snapshots/list published it. No Authorization header is needed with it. The whole URL is published ready to use: it is this base plus this path, and nothing has to be assembled.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "snapshot_key",
            "in": "query",
            "required": false,
            "description": "The snapshot, from /v1/snapshots/list. Requires the Bearer of the account.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The file.",
            "headers": {
              "Content-Disposition": {
                "description": "attachment with the name of the file, reduced to ASCII.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/vnd.ms-excel": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/filters/create": {
      "post": {
        "operationId": "createFilter",
        "tags": [
          "Filters"
        ],
        "summary": "Create a saved filter",
        "description": "Creates a saved filter of the account: a named set of conditions that widgets, sections, portal views, calculated metrics and Data Studio account groups point at by key.\n\nUSE A SAVED FILTER FOR WHAT IS SHARED. A filter that belongs to one widget is written in the filters block of that widget's definition and nowhere else; this endpoint is for the conditions several elements are meant to read and to change in one place. A widget points at a saved filter by naming its key in filter_key -- of the whole widget, or of ONE COLUMN of a table_metrics_summary, writing that entry of metrics[] as {id, filter_key} -- and filter_key excludes the filters block, so a widget reads a saved filter or carries conditions of its own and never both.\n\nconditions IS THE FILTERS BLOCK OF THE WIDGET DSL, checked by the very code that checks a widget's own filter -- with one rule of its own, because a saved filter is read inside ONE data view where a widget is not: the field of every condition has to be one that view offers, so a filter of the campaign resource of Google Ads carrying a condition on a field of ad_group is refused with FILTER_FIELD_NOT_IN_DATA_VIEW instead of being saved as a definition every widget reads back empty. Beyond that: the field ids and the operators are the public vocabulary of the integration (/v1/fields and /v1/integrations/detail), the combination is the one the integration declares, and type_of_field is present in every condition with null as its value unless the integration asks for one (\"type_of_field\": null unless the integration requires it), which is the rule a widget's own filters follow. For ga4, a saved filter takes standard fields only: the custom dimensions and metrics of a property can filter a widget or a query, not a saved filter, which is not tied to one property. What /v1/filters/get publishes under conditions can be sent here as it is.\n\nintegration_id and level cannot be changed afterwards. level is required for the integrations capabilities lists under filters.level_required_for -- the ones whose filters are read in a data view -- and refused for the rest.\n\nThe filter is born as the account's own: is_system_defined is never written by this API.\n\nif_version is not accepted, and the call is never confirmed: its inverse is /v1/filters/delete.\n\nRefusals: 400 for the framework, the body and the conditions (VALIDATION_FAILED, with the problem codes of the widget DSL and the path of the condition that carries them), 403 WRITE_ACCESS_DISABLED.\n\nA definition written through this API is not visible to an application window that is already open: the page has to be reloaded before the same definition is edited there.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the create_filter tool.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FiltersCreateRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "name": "Brand campaigns",
                    "integration_id": "google_ads",
                    "level": "campaign",
                    "conditions": {
                      "combination": "and",
                      "conditions": [
                        {
                          "field": "campaign_name",
                          "operator": "LIKE",
                          "value": "%brand%",
                          "type_of_field": null
                        }
                      ]
                    },
                    "idempotency_key": "acme-brand-filter-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FiltersCreateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The filter is written and read back in the shape /v1/filters/get publishes.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "filter": {
                          "filter_key": "5c2a9f1b7e3d40a8b6c4e2f819d07a35",
                          "name": "Brand campaigns",
                          "display_name": "Brand campaigns",
                          "internal_notes": null,
                          "integration_id": "google_ads",
                          "level": "campaign",
                          "is_system_defined": false,
                          "conditions": {
                            "combination": "and",
                            "conditions": [
                              {
                                "field": "campaign_name",
                                "operator": "LIKE",
                                "value": "%brand%",
                                "type_of_field": null
                              }
                            ]
                          },
                          "not_expressible_reason": null,
                          "not_expressible_message": null
                        },
                        "created_keys": [
                          "5c2a9f1b7e3d40a8b6c4e2f819d07a35"
                        ],
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-21T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/filters/update": {
      "post": {
        "operationId": "updateFilter",
        "tags": [
          "Filters"
        ],
        "summary": "Update a saved filter",
        "description": "Changes a saved filter of the account.\n\nIT EMPTIES EVERY CACHED RESULT OF THE ACCOUNT -- the results of the widgets of every report and the cached answers of Data Studio -- exactly as the application does when the same filter is saved there: everything that reads the filter was computed with the definition that has just changed. That is also the supported way to drop that cache on demand. It is EXPENSIVE on a large account, because the platform reads those rows before deleting them, so do not chain calls of this endpoint. The answer of the write carries dropped_account_cache: true and a DROPS_ACCOUNT_CACHE warning; a dry run carries the warning alone, because it empties nothing. Creating and DELETING a filter empty nothing.\n\nIt is a PARTIAL write: what the body does not carry is left as it is, and conditions replaces the whole set. integration_id and level cannot change -- the conditions are fields of that data view -- and a body that names a different one is refused; one that names the same is accepted, so the object a read answered can be sent back.\n\nA filter the platform ships answers 409 NOT_ALLOWED with details.reason system_defined, which is what the editor of the application does too.\n\nThe answer carries `before` with the whole previous definition: sending it back through this endpoint is the inverse of the call, and it empties the cache a second time.\n\nif_version is not accepted.\n\nRefusals: 400 for the body and the conditions, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND when the filter is not this account's, and 409 NOT_ALLOWED for a system one. NOTHING IS EMPTIED IN ANY OF THOSE CASES: every refusal is decided before the platform is asked to write, and the cached results are emptied only once the new definition has been stored. A 500 that could not store it says so with details.dropped_account_cache false.\n\nA definition written through this API is not visible to an application window that is already open: the page has to be reloaded before the same definition is edited there.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the update_filter tool. It empties every cached result of the account.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FiltersUpdateRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "filter_key": "5c2a9f1b7e3d40a8b6c4e2f819d07a35",
                    "conditions": {
                      "combination": "and",
                      "conditions": [
                        {
                          "field": "campaign_name",
                          "operator": "LIKE",
                          "value": "%brand%",
                          "type_of_field": null
                        },
                        {
                          "field": "campaign_status",
                          "operator": "EQUALS",
                          "value": "ENABLED",
                          "type_of_field": null
                        }
                      ]
                    },
                    "idempotency_key": "acme-brand-filter-add-status-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FiltersUpdateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The filter is written, the cache of the account is empty, and before is the body that undoes it.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "filter": {
                          "filter_key": "5c2a9f1b7e3d40a8b6c4e2f819d07a35",
                          "name": "Brand campaigns",
                          "display_name": "Brand campaigns",
                          "internal_notes": null,
                          "integration_id": "google_ads",
                          "level": "campaign",
                          "is_system_defined": false,
                          "conditions": {
                            "combination": "and",
                            "conditions": [
                              {
                                "field": "campaign_name",
                                "operator": "LIKE",
                                "value": "%brand%",
                                "type_of_field": null
                              }
                            ]
                          },
                          "not_expressible_reason": null,
                          "not_expressible_message": null
                        },
                        "before": {
                          "filter_key": "5c2a9f1b7e3d40a8b6c4e2f819d07a35",
                          "name": "Brand campaigns",
                          "display_name": "Brand campaigns",
                          "internal_notes": null,
                          "integration_id": "google_ads",
                          "level": "campaign",
                          "is_system_defined": false,
                          "conditions": {
                            "combination": "and",
                            "conditions": [
                              {
                                "field": "campaign_name",
                                "operator": "LIKE",
                                "value": "%brand%",
                                "type_of_field": null
                              }
                            ]
                          },
                          "not_expressible_reason": null,
                          "not_expressible_message": null
                        },
                        "dropped_account_cache": true,
                        "unfulfilled": [],
                        "warnings": [
                          {
                            "path": "",
                            "code": "DROPS_ACCOUNT_CACHE",
                            "message": "updating a filter empties every cached result of the account, reports and Data Studio alike, exactly as the application does it. It is expensive on a large account: do not chain it. Deleting a filter does not empty anything",
                            "suggestions": []
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-21T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/filters/delete": {
      "post": {
        "operationId": "deleteFilter",
        "tags": [
          "Filters"
        ],
        "summary": "Delete a saved filter",
        "description": "Deletes a saved filter of the account.\n\nTHERE IS NO WAY BACK. /v1/filters/create writes a NEW filter, with a new key, and everything that read the deleted one would have to be pointed at it; that is why this endpoint ALWAYS asks for a confirmation and why its answer carries `before` with the whole definition, which is what it would take to write it again.\n\nIT REFUSES WHILE ANYTHING STILL READS IT: 409 IN_USE, with details.used_by naming what, in the shape of the EntityUsage schema -- reports (each with its report_key, its internal name, whether it is in the trash, and how many widgets and sections of it read the filter), calculated metrics, portal views, how many alerts, and how many Data Studio account groups. Reports in the trash count, like everywhere else in this API. A report_key there can be the key of a TEMPLATE, because a widget of a template based report belongs to the template, and then there is no internal name to give; it is null for an element whose report could not be resolved, and that entry then stands for the element alone. Answering that question costs twenty queries, which is why no read publishes it.\n\nIt does NOT empty the cached results of the account: only an update does (dropped_account_cache is false here).\n\nA filter the platform ships answers 409 NOT_ALLOWED with details.reason system_defined.\n\nif_version is not accepted.\n\nRefusals: 400 for the framework and the body, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND when the filter is not this account's, 409 IN_USE and 409 NOT_ALLOWED.\n\nA definition written through this API is not visible to an application window that is already open: the page has to be reloaded before the same definition is edited there.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the delete_filter tool. It is refused while anything still reads the filter.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FiltersDeleteRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "filter_key": "5c2a9f1b7e3d40a8b6c4e2f819d07a35",
                    "idempotency_key": "drop-acme-brand-filter-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FiltersDeleteResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The first call answers a token; the second is this same body with confirmation_token added.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "filter_key": "5c2a9f1b7e3d40a8b6c4e2f819d07a35",
                        "before": {
                          "filter_key": "5c2a9f1b7e3d40a8b6c4e2f819d07a35",
                          "name": "Brand campaigns",
                          "display_name": "Brand campaigns",
                          "internal_notes": null,
                          "integration_id": "google_ads",
                          "level": "campaign",
                          "is_system_defined": false,
                          "conditions": {
                            "combination": "and",
                            "conditions": [
                              {
                                "field": "campaign_name",
                                "operator": "LIKE",
                                "value": "%brand%",
                                "type_of_field": null
                              }
                            ]
                          },
                          "not_expressible_reason": null,
                          "not_expressible_message": null
                        },
                        "dropped_account_cache": false,
                        "unfulfilled": [],
                        "warnings": [],
                        "deleted": true
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-21T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/calculated-metrics/create": {
      "post": {
        "operationId": "createCalculatedMetric",
        "tags": [
          "Calculated Metrics"
        ],
        "summary": "Create a calculated metric",
        "description": "Creates a calculated metric of the account: a column the widgets of that integration can draw like any other, and that /v1/fields publishes from then on.\n\nTHREE KINDS, AND THE BODY CARRIES THE BLOCK OF ONE. expression is a formula over the metrics of the integration; function aggregates one metric over a day, a week or a month; filtered reads one metric through a saved filter of the same integration. The two blocks that are not this metric's kind are left out or sent as null, which is what a read publishes for them.\n\nTHE IDS OF A FORMULA ARE CHECKED, which is stricter than the application and is deliberate: the editor calls a token a metric as soon as it starts with the prefix of an integration, so a retired id is saved and drawn as an empty column for ever. Here every token that is not an operator, a parenthesis or a number has to be a metric /v1/fields publishes for the integration, and anything else is refused by name. The rest of the rules are the editor's, one for one: a formula does not begin with an operator or with ')', does not end with an operator or with '(', carries no forbidden pair of consecutive tokens, carries at least one metric, and balances its parentheses.\n\nintegration_id, type and level cannot be changed afterwards. The metric is born as the account's own: is_system_defined and the signature of a catalogue definition are never written by this API.\n\nif_version is not accepted, and the call is never confirmed: its inverse is /v1/calculated-metrics/delete.\n\nRefusals: 400 for the framework, the body and the formula, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND when a filtered metric names a filter that is not this account's.\n\nA definition written through this API is not visible to an application window that is already open: the page has to be reloaded before the same definition is edited there.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the create_calculated_metric tool.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CalculatedMetricsCreateRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "name": "Cost per lead",
                    "integration_id": "google_ads",
                    "level": "campaign",
                    "type": "expression",
                    "expression": {
                      "format": "currency",
                      "formula": [
                        "cost",
                        "/",
                        "conversions"
                      ],
                      "green_when_increases": false
                    },
                    "idempotency_key": "acme-cpl-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CalculatedMetricsCreateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The metric is written and /v1/fields publishes it from now on.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "calculated_metric": {
                          "calculated_metric_key": "c73f1e8a4b26d5093ef7a1c60b482d95",
                          "name": "Cost per lead",
                          "display_name": "Cost per lead",
                          "internal_notes": null,
                          "integration_id": "google_ads",
                          "level": "campaign",
                          "type": "expression",
                          "is_system_defined": false,
                          "expression": {
                            "format": "currency",
                            "formula": [
                              "cost",
                              "/",
                              "conversions"
                            ],
                            "green_when_increases": false
                          },
                          "function": null,
                          "filtered": null,
                          "not_expressible_reason": null,
                          "not_expressible_message": null
                        },
                        "created_keys": [
                          "c73f1e8a4b26d5093ef7a1c60b482d95"
                        ],
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-21T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/calculated-metrics/update": {
      "post": {
        "operationId": "updateCalculatedMetric",
        "tags": [
          "Calculated Metrics"
        ],
        "summary": "Update a calculated metric",
        "description": "Changes a calculated metric of the account.\n\nIT EMPTIES EVERY CACHED RESULT OF THE ACCOUNT, like /v1/filters/update and for the same reason: every widget that draws the metric was computed with the definition that has just changed. It is expensive on a large account, so do not chain calls of this endpoint. Creating and deleting a metric empty nothing.\n\nIt is a PARTIAL write, inside the block of its kind too: what the body does not name is left as it is. type, integration_id and level cannot change -- each kind is stored in columns of its own and the definition names fields of that data view -- and a body that names what the metric already is is accepted, so a read can be replayed.\n\nONLY is_system_defined STOPS AN UPDATE, which is what the editor of the application looks at and at nothing else: a row that carries the stable signature of a catalogue definition and is not marked as the platform's is edited on both surfaces. A metric the platform ships answers 409 NOT_ALLOWED with details.reason system_defined, decided before anything is written.\n\nThe two legacy formats a read may publish, time_millis and text, are kept by an update that does not name format and cannot be written: only the four of the editor are accepted.\n\nThe answer carries `before` with the whole previous definition, which is the body that undoes the call.\n\nif_version is not accepted.\n\nRefusals: 400 for the body and the formula, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, and 409 NOT_ALLOWED for a system one. Nothing is emptied in any of those cases: the cached results go only once the new definition has been stored, and a 500 that could not store it says so with details.dropped_account_cache false.\n\nA definition written through this API is not visible to an application window that is already open: the page has to be reloaded before the same definition is edited there.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the update_calculated_metric tool. It empties every cached result of the account.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CalculatedMetricsUpdateRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "calculated_metric_key": "c73f1e8a4b26d5093ef7a1c60b482d95",
                    "expression": {
                      "formula": [
                        "cost",
                        "/",
                        "all_conversions"
                      ]
                    },
                    "idempotency_key": "acme-cpl-all-conversions-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CalculatedMetricsUpdateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The formula is written and before is the body that undoes it.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "calculated_metric": {
                          "calculated_metric_key": "c73f1e8a4b26d5093ef7a1c60b482d95",
                          "name": "Cost per lead",
                          "display_name": "Cost per lead",
                          "internal_notes": null,
                          "integration_id": "google_ads",
                          "level": "campaign",
                          "type": "expression",
                          "is_system_defined": false,
                          "expression": {
                            "format": "currency",
                            "formula": [
                              "cost",
                              "/",
                              "conversions"
                            ],
                            "green_when_increases": false
                          },
                          "function": null,
                          "filtered": null,
                          "not_expressible_reason": null,
                          "not_expressible_message": null
                        },
                        "before": {
                          "calculated_metric_key": "c73f1e8a4b26d5093ef7a1c60b482d95",
                          "name": "Cost per lead",
                          "display_name": "Cost per lead",
                          "internal_notes": null,
                          "integration_id": "google_ads",
                          "level": "campaign",
                          "type": "expression",
                          "is_system_defined": false,
                          "expression": {
                            "format": "currency",
                            "formula": [
                              "cost",
                              "/",
                              "conversions"
                            ],
                            "green_when_increases": false
                          },
                          "function": null,
                          "filtered": null,
                          "not_expressible_reason": null,
                          "not_expressible_message": null
                        },
                        "dropped_account_cache": true,
                        "unfulfilled": [],
                        "warnings": [
                          {
                            "path": "",
                            "code": "DROPS_ACCOUNT_CACHE",
                            "message": "updating a calculated metric empties every cached result of the account, reports and Data Studio alike, exactly as the application does it. It is expensive on a large account: do not chain it. Deleting one does not empty anything",
                            "suggestions": []
                          }
                        ]
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-21T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/calculated-metrics/delete": {
      "post": {
        "operationId": "deleteCalculatedMetric",
        "tags": [
          "Calculated Metrics"
        ],
        "summary": "Delete a calculated metric",
        "description": "Deletes a calculated metric of the account.\n\nTHERE IS NO WAY BACK: /v1/calculated-metrics/create writes a NEW metric, with a new key, and every widget that drew the old one would have to be changed. That is why this endpoint ALWAYS asks for a confirmation and why its answer carries `before` with the whole definition.\n\nIT REFUSES WHILE ANYTHING STILL DRAWS IT: 409 IN_USE, with details.used_by in the shape of the EntityUsage schema. It carries `reports` -- each with its report_key, its internal name, whether it is in the trash and how many widgets of it draw the metric -- and `account_groups`, how many Data Studio account groups do, AND NOTHING ELSE: the question asked of a calculated metric covers those two families, so the calculated_metrics, portal_views and alerts a filter's used_by carries are absent here rather than published empty. A report_key can be the key of a TEMPLATE, and it is null for an element whose report could not be resolved.\n\nIt does NOT empty the cached results of the account.\n\nA metric the platform ships answers 409 NOT_ALLOWED with details.reason system_defined.\n\nif_version is not accepted.\n\nRefusals: 400 for the framework and the body, 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 IN_USE and 409 NOT_ALLOWED.\n\nA definition written through this API is not visible to an application window that is already open: the page has to be reloaded before the same definition is edited there.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the delete_calculated_metric tool. It is refused while any widget still draws the metric.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CalculatedMetricsDeleteRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "calculated_metric_key": "c73f1e8a4b26d5093ef7a1c60b482d95",
                    "idempotency_key": "drop-acme-cpl-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CalculatedMetricsDeleteResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The first call answers a token; the second is this same body with confirmation_token added.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "calculated_metric_key": "c73f1e8a4b26d5093ef7a1c60b482d95",
                        "before": {
                          "calculated_metric_key": "c73f1e8a4b26d5093ef7a1c60b482d95",
                          "name": "Cost per lead",
                          "display_name": "Cost per lead",
                          "internal_notes": null,
                          "integration_id": "google_ads",
                          "level": "campaign",
                          "type": "expression",
                          "is_system_defined": false,
                          "expression": {
                            "format": "currency",
                            "formula": [
                              "cost",
                              "/",
                              "conversions"
                            ],
                            "green_when_increases": false
                          },
                          "function": null,
                          "filtered": null,
                          "not_expressible_reason": null,
                          "not_expressible_message": null
                        },
                        "dropped_account_cache": false,
                        "unfulfilled": [],
                        "warnings": [],
                        "deleted": true
                      },
                      "meta": {
                        "request_id": "req_a1b2c3d4e5f6",
                        "timestamp": "2026-09-21T10:30:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/email-templates/get": {
      "post": {
        "operationId": "getEmailTemplate",
        "tags": [
          "Email Templates"
        ],
        "summary": "Get an email template",
        "description": "Returns one email template of the account WITH its body, which /v1/email-templates/list leaves out because it runs to 75.000 characters.\n\nThe object this answers is the body /v1/email-templates/update takes: read a template before changing it, because an update is partial and a body written from memory is a body replaced by a summary of itself.\n\nThe system template -- the one a delivery uses when it names no template of the account -- is not a template of the account and is not readable here.",
        "x-mcp-description": "Use the get_email_template tool.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailTemplatesGetRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "email_template_key": "9a1c4e7b2d5f40839c6e1b7a5d204f68"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailTemplatesGetResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The answer of the call.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "email_template": {
                          "email_template_key": "9a1c4e7b2d5f40839c6e1b7a5d204f68",
                          "name": "Monthly report",
                          "internal_notes": null,
                          "format": "html",
                          "sender": "Acme Analytics",
                          "sender_email": "reports@acme.com",
                          "reply_to_email": "reports@acme.com",
                          "subject": "%%REPORT_TITLE%% - %%CURRENT_PERIOD%%",
                          "body": "<html><body><p>Your report is ready: %%REPORT_LINK%%</p></body></html>"
                        }
                      },
                      "meta": {
                        "request_id": "req_01J8Z0X9",
                        "timestamp": "2026-09-21T10:00:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/email-templates/create": {
      "post": {
        "operationId": "createEmailTemplate",
        "tags": [
          "Email Templates"
        ],
        "summary": "Create an email template",
        "description": "Creates an email template of the account: the email a scheduled delivery is sent as, reused by every schedule that names it with email_template_key.\n\nIT SENDS NOTHING. Writing a template puts no mail in anybody's inbox -- only a schedule does -- and it does not count against the daily email budget of the account.\n\nTHE SENDER HAS TO BE THE ACCOUNT'S. sender_email and reply_to_email have to be the address of a user of the account or share the domain of one, which is the rule /v1/schedules/create applies and the application does not: a template written through an API key would otherwise send mail as anybody.\n\nTHE BODY IS STORED AS IT IS. It is not sanitized: a template is a whole HTML document and the application never paints it -- it travels by email. What is refused is the content the platform blocks for a delivery, the same check a schedule goes through.\n\nPLACEHOLDERS. The subject and the body can carry %%REPORT_TITLE%%, %%CURRENT_PERIOD%%, %%PAST_PERIOD%%, %%EXECUTION_DATE%%, %%CUSTOM_VAR_1%% to %%CUSTOM_VAR_5%%, %%REPORT_LINK%% and %%AI_SUMMARY%% (body only), replaced when the email goes out. %%REPORT_LINK%% is required in the body of a delivery sent in the link format, which /v1/schedules/create checks and this endpoint does not: a template does not know which deliveries will use it.\n\nif_version is not accepted, and the call is never confirmed: its inverse is /v1/email-templates/delete.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the create_email_template tool.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailTemplatesCreateRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "name": "Monthly report",
                    "format": "html",
                    "sender": "Acme Analytics",
                    "sender_email": "reports@acme.com",
                    "reply_to_email": "reports@acme.com",
                    "subject": "%%REPORT_TITLE%% - %%CURRENT_PERIOD%%",
                    "body": "<html><body><p>Your report is ready: %%REPORT_LINK%%</p></body></html>",
                    "idempotency_key": "acme-template-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailTemplatesCreateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The answer of the call.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "email_template": {
                          "email_template_key": "9a1c4e7b2d5f40839c6e1b7a5d204f68",
                          "name": "Monthly report",
                          "internal_notes": null,
                          "format": "html",
                          "sender": "Acme Analytics",
                          "sender_email": "reports@acme.com",
                          "reply_to_email": "reports@acme.com",
                          "subject": "%%REPORT_TITLE%% - %%CURRENT_PERIOD%%",
                          "body": "<html><body><p>Your report is ready: %%REPORT_LINK%%</p></body></html>"
                        },
                        "created_keys": [
                          "9a1c4e7b2d5f40839c6e1b7a5d204f68"
                        ],
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_01J8Z0X9",
                        "timestamp": "2026-09-21T10:00:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/email-templates/update": {
      "post": {
        "operationId": "updateEmailTemplate",
        "tags": [
          "Email Templates"
        ],
        "summary": "Update an email template",
        "description": "Changes an email template of the account. It is a PARTIAL write: what the body does not name is left as it is, and the answer carries `before`, which is the body that puts the template back.\n\nEVERY SCHEDULE THAT NAMES IT is sent with the new text from the next delivery onwards -- /v1/schedules/list says which ones -- so read the template with /v1/email-templates/get before changing it. Nothing is sent by this call and no cache is emptied.\n\nThe same rules as a creation apply to the values this body carries: the sender has to belong to the account, the body is stored as it is and the refused content of the platform is refused here too. The one template with a rule of its own is the one the client portal sends its notifications with: its body has to keep %%BODY%%, which is where the portal writes its message.\n\nThe object /v1/email-templates/get answers can be sent back as it is.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the update_email_template tool.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailTemplatesUpdateRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "email_template_key": "9a1c4e7b2d5f40839c6e1b7a5d204f68",
                    "subject": "%%REPORT_TITLE%% - %%CURRENT_PERIOD%% results",
                    "idempotency_key": "acme-template-2"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailTemplatesUpdateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The answer of the call.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "email_template": {
                          "email_template_key": "9a1c4e7b2d5f40839c6e1b7a5d204f68",
                          "name": "Monthly report",
                          "internal_notes": null,
                          "format": "html",
                          "sender": "Acme Analytics",
                          "sender_email": "reports@acme.com",
                          "reply_to_email": "reports@acme.com",
                          "subject": "%%REPORT_TITLE%% - %%CURRENT_PERIOD%%",
                          "body": "<html><body><p>Your report is ready: %%REPORT_LINK%%</p></body></html>"
                        },
                        "before": {
                          "email_template_key": "9a1c4e7b2d5f40839c6e1b7a5d204f68",
                          "name": "Monthly report",
                          "internal_notes": null,
                          "format": "html",
                          "sender": "Acme Analytics",
                          "sender_email": "reports@acme.com",
                          "reply_to_email": "reports@acme.com",
                          "subject": "%%REPORT_TITLE%% - %%CURRENT_PERIOD%%",
                          "body": "<html><body><p>Your report is ready: %%REPORT_LINK%%</p></body></html>"
                        },
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_01J8Z0X9",
                        "timestamp": "2026-09-21T10:00:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/email-templates/delete": {
      "post": {
        "operationId": "deleteEmailTemplate",
        "tags": [
          "Email Templates"
        ],
        "summary": "Delete an email template",
        "description": "Deletes an email template of the account. It asks for a confirmation and THERE IS NO WAY BACK: /v1/email-templates/create writes a new template with a new key, and every schedule that read the deleted one would have to be pointed at it.\n\nIt is refused with 409 IN_USE while any scheduled delivery is sent with the template, and details.used_by.schedules names them with their key, their report and their frequency. It is refused the same way, with details.reason client_portal, when it is the template the client portal sends its notifications with; which template that is, is chosen in the application.\n\nThe answer carries `before` with the whole template, body included, which is what it would take to write it again.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the delete_email_template tool.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailTemplatesDeleteRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "email_template_key": "9a1c4e7b2d5f40839c6e1b7a5d204f68",
                    "idempotency_key": "acme-template-3",
                    "confirmation_token": "cnf_01J8Z0X9ABCD"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailTemplatesDeleteResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The answer of the call.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "email_template_key": "9a1c4e7b2d5f40839c6e1b7a5d204f68",
                        "before": {
                          "email_template_key": "9a1c4e7b2d5f40839c6e1b7a5d204f68",
                          "name": "Monthly report",
                          "internal_notes": null,
                          "format": "html",
                          "sender": "Acme Analytics",
                          "sender_email": "reports@acme.com",
                          "reply_to_email": "reports@acme.com",
                          "subject": "%%REPORT_TITLE%% - %%CURRENT_PERIOD%%",
                          "body": "<html><body><p>Your report is ready: %%REPORT_LINK%%</p></body></html>"
                        },
                        "deleted": true,
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_01J8Z0X9",
                        "timestamp": "2026-09-21T10:00:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/language-files/get": {
      "post": {
        "operationId": "getLanguageFile",
        "tags": [
          "Language Files"
        ],
        "summary": "Get a language file",
        "description": "Returns one language file -- one of the account's own or one of the sixteen the platform ships -- with a signed link to its .txt.\n\nTHE LITERALS NEVER TRAVEL IN JSON. A language file is six thousand lines and 370 to 420 KB, which is over half a megabyte once base64 encoded: download_url is a link to GET /v1/language-files/download that carries the account and the file inside it, needs no Authorization header and expires 900 seconds after this call (download_url_expires_at says when). Ask again for a fresh one.\n\nIt is the way to get the .txt that /v1/language-files/create and /v1/language-files/update take: download a file, edit the values, send it back.",
        "x-mcp-description": "Use the get_language_file tool.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LanguageFilesGetRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "language_file_key": "language_file_es"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LanguageFilesGetResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The answer of the call.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "language_file": {
                          "language_file_key": "4b8e0d2a6c1f47b9a3d5e7c9018f2b46",
                          "name": "Spanish - Acme",
                          "language": "es",
                          "is_system": false,
                          "file_name": "language_file_es.txt",
                          "internal_notes": null,
                          "download_url": "https://api.reportingninja.com/v1/language-files/download?token=Xk7...",
                          "download_url_expires_at": "2026-09-21T10:15:00Z"
                        }
                      },
                      "meta": {
                        "request_id": "req_01J8Z0X9",
                        "timestamp": "2026-09-21T10:00:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/language-files/create": {
      "post": {
        "operationId": "createLanguageFile",
        "tags": [
          "Language Files"
        ],
        "summary": "Create a language file",
        "description": "Creates a language file of the account: the literals the widgets, the headers and the field names of a report are printed with, which /v1/reports/update then puts on a report with language_file.\n\nBY FILE AND NOT LITERAL BY LITERAL, which is how the application writes one: file.content_base64 carries a .txt and the whole set of literals is stored from it. Download one with /v1/language-files/get -- one of the sixteen the platform ships is the usual starting point -- edit the values and send it here.\n\nTHE PARSER IS THE APPLICATION'S. UTF-8, one `key = value` per line, # for a comment, an apostrophe written as &#39;. A line with two equals signs, a key that is there twice or a backslash is refused with the message of the parser, naming the line. Keys no field catalogue of the platform knows are DROPPED and counted: the answer says ignored_keys and carries a LANGUAGE_FILE_KEYS_IGNORED warning.\n\nlanguage is required: it is the ISO 639-1 code the literals are written in, and what the AI summaries of every report using this file are written in.\n\nCreating a file changes no report until /v1/reports/update points one at it, and it empties no cached result.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the create_language_file tool.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LanguageFilesCreateRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "name": "Spanish - Acme",
                    "language": "es",
                    "file": {
                      "name": "language_file_es.txt",
                      "content_base64": "IyBTcGFuaXNoIC0gQWNtZQpUT1RBTF9MSVRFUkFMID0gVG90YWwKUEFHRV9MSVRFUkFMID0gUGFnaW5hCk9GX0xJVEVSQUwgPSBkZQo="
                    },
                    "idempotency_key": "acme-language-1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LanguageFilesCreateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The answer of the call.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "language_file": {
                          "language_file_key": "4b8e0d2a6c1f47b9a3d5e7c9018f2b46",
                          "name": "Spanish - Acme",
                          "language": "es",
                          "is_system": false,
                          "file_name": "language_file_es.txt",
                          "internal_notes": null
                        },
                        "ignored_keys": 0,
                        "created_keys": [
                          "4b8e0d2a6c1f47b9a3d5e7c9018f2b46"
                        ],
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_01J8Z0X9",
                        "timestamp": "2026-09-21T10:00:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/language-files/update": {
      "post": {
        "operationId": "updateLanguageFile",
        "tags": [
          "Language Files"
        ],
        "summary": "Update a language file",
        "description": "Changes a language file of the account. It is a PARTIAL write over the columns -- name, language, internal_notes -- and `file` is the one field that is not one of them.\n\nWITH file EVERY LITERAL IS REPLACED by the ones of the .txt that is sent, which is what uploading a file again does in the application; WITHOUT it not a single literal is touched. There is no partial edit of literals on this surface, and the .txt a call replaced is not kept anywhere: `before` carries the columns and not the literals, so the inverse of a call that carried a file is uploading the previous file again.\n\nNOTHING IS RECOMPUTED BY THE CALL. When the file changes the literals, the cached results of every report printed with this file are dropped, so each one shows the new literals the next time its widgets are drawn; a call that sends the same literals again, or no file, drops nothing. A window of the application that is already open keeps the previous literals until it is reloaded, and the results it draws meanwhile can be stored again with the old literals and served to the reports printed with the file for up to four hours. The drop is best effort: a call that could not clear the stored results still answers 200.\n\nThe sixteen files the platform ships and the empty key answer 409 NOT_ALLOWED with details.reason system_defined.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the update_language_file tool.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LanguageFilesUpdateRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "language_file_key": "4b8e0d2a6c1f47b9a3d5e7c9018f2b46",
                    "name": "Spanish - Acme (2026)",
                    "idempotency_key": "acme-language-2"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LanguageFilesUpdateResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The answer of the call.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "language_file": {
                          "language_file_key": "4b8e0d2a6c1f47b9a3d5e7c9018f2b46",
                          "name": "Spanish - Acme",
                          "language": "es",
                          "is_system": false,
                          "file_name": "language_file_es.txt",
                          "internal_notes": null
                        },
                        "before": {
                          "language_file_key": "4b8e0d2a6c1f47b9a3d5e7c9018f2b46",
                          "name": "Spanish - Acme",
                          "language": "es",
                          "is_system": false,
                          "file_name": "language_file_es.txt",
                          "internal_notes": null
                        },
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_01J8Z0X9",
                        "timestamp": "2026-09-21T10:00:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/language-files/delete": {
      "post": {
        "operationId": "deleteLanguageFile",
        "tags": [
          "Language Files"
        ],
        "summary": "Delete a language file",
        "description": "Deletes a language file of the account, with all its literals. It asks for a confirmation and THERE IS NO WAY BACK: nothing keeps a copy of the .txt, so download it with /v1/language-files/get first if it may be wanted again.\n\nIt is refused with 409 IN_USE while any report of the account is printed with it, and details.used_by.reports names them -- reports in the trash included, because they come back pointing at it. The sixteen files the platform ships and the empty key -- what a report stores when it prints with no language file at all -- answer 409 NOT_ALLOWED with details.reason system_defined.\n\nEvery write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.",
        "x-mcp-description": "Use the delete_language_file tool.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LanguageFilesDeleteRequest"
              },
              "examples": {
                "example": {
                  "summary": "A call of this endpoint.",
                  "value": {
                    "language_file_key": "4b8e0d2a6c1f47b9a3d5e7c9018f2b46",
                    "idempotency_key": "acme-language-3",
                    "confirmation_token": "cnf_01J8Z0X9ABCD"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LanguageFilesDeleteResponse"
                },
                "examples": {
                  "ok": {
                    "summary": "The answer of the call.",
                    "value": {
                      "status": "ok",
                      "data": {
                        "language_file_key": "4b8e0d2a6c1f47b9a3d5e7c9018f2b46",
                        "before": {
                          "language_file_key": "4b8e0d2a6c1f47b9a3d5e7c9018f2b46",
                          "name": "Spanish - Acme",
                          "language": "es",
                          "is_system": false,
                          "file_name": "language_file_es.txt",
                          "internal_notes": null
                        },
                        "deleted": true,
                        "unfulfilled": [],
                        "warnings": []
                      },
                      "meta": {
                        "request_id": "req_01J8Z0X9",
                        "timestamp": "2026-09-21T10:00:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/WriteTemporarilyUnavailable"
          }
        }
      }
    },
    "/language-files/download": {
      "get": {
        "operationId": "downloadLanguageFile",
        "tags": [
          "Language Files"
        ],
        "summary": "Download the .txt of a language file",
        "description": "Answers the .txt of a language file: `text/plain; charset=UTF-8` with a Content-Disposition of language_file.txt. One of the two routes of this API that are a GET, whose success is not a JSON envelope and that have no MCP tool of their own.\n\nTWO WAYS IN, AND ONE OF THEM IS ENOUGH. `token` is the signed link /v1/language-files/get publishes as download_url: it carries the account and the file inside it, needs no Authorization header -- which is what lets an assistant hand it to a person -- and expires 900 seconds after it was minted, with no way to revoke it before that. `language_file_key` is the form a script uses, with the Bearer of the account. A request carrying both is served by the token.\n\nTHE FILE IS REBUILT, not stored: the English master of the platform, with its sections, its comments and its keys, carrying the literals of this file. That is what the download of the application answers and what /v1/language-files/create reads back, so the round trip is closed. The sixteen files of the platform are downloadable too.\n\nEverything that is not the file comes back in the envelope of the rest of the API (401, 404, 429). A POST to this path is ENDPOINT_NOT_FOUND, and a HEAD is refused with 405.",
        "x-mcp-description": "No tool: a tool does not return a file. get_language_file answers the signed link.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The signed link of /v1/language-files/get. No Authorization header is needed with it."
          },
          {
            "name": "language_file_key",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The file, with the Bearer of the account. Excluded by token."
          }
        ],
        "responses": {
          "200": {
            "description": "The .txt of the language file.",
            "headers": {
              "Content-Disposition": {
                "description": "attachment; filename=\"language_file.txt\"",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your Reporting Ninja public API key, obtained from the REST API tab in the app."
      }
    },
    "schemas": {
      "EmptyBody": {
        "type": "object",
        "description": "No parameters."
      },
      "Meta": {
        "type": "object",
        "required": [
          "request_id",
          "timestamp"
        ],
        "properties": {
          "request_id": {
            "type": "string",
            "description": "Id of this request, useful when reporting an incident."
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "When the answer was built, in UTC."
          }
        }
      },
      "ErrorEnvelope": {
        "type": "object",
        "required": [
          "status",
          "error_code",
          "message",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "error"
            ]
          },
          "error_code": {
            "type": "string",
            "description": "Machine-readable code that classifies the error. Stable across versions. Clients should switch on this code instead of parsing the `message` text. Reading the HTTP status alone is not enough: the data endpoints answer their business errors with HTTP 200 and status=error, while the report endpoints answer with the real status.",
            "enum": [
              "AUTH_INVALID",
              "AUTH_EXPIRED",
              "RATE_LIMITED",
              "WRITE_ACCESS_DISABLED",
              "WRITE_TEMPORARILY_UNAVAILABLE",
              "NOT_FOUND",
              "ENDPOINT_NOT_FOUND",
              "INVALID_REQUEST",
              "INVALID_MODE",
              "PAYLOAD_TOO_LARGE",
              "MISSING_REQUIRED_FIELD",
              "INVALID_VALUE",
              "INVALID_LIMIT",
              "UNKNOWN_PARAM",
              "INTEGRATION_NOT_SUPPORTED",
              "INTEGRATION_NOT_IMPLEMENTED",
              "INTEGRATION_NOT_QUERYABLE",
              "FIELD_NOT_AVAILABLE",
              "FILTER_NOT_WIRED",
              "INVALID_FILTER",
              "INVALID_DATE_RANGE",
              "CONNECTION_NOT_FOUND",
              "PLAN_LIMIT_REACHED",
              "PROVIDER_PERMISSION",
              "PROVIDER_UNAVAILABLE",
              "JOB_NOT_FOUND",
              "CURSOR_EXPIRED",
              "VALIDATION_FAILED",
              "APPEND_NOT_ALLOWED",
              "TEMPLATE_NOT_FOUND",
              "CONFIRMATION_EXPIRED",
              "CONFIRMATION_MISMATCH",
              "IDEMPOTENCY_CONFLICT",
              "IDEMPOTENCY_IN_PROGRESS",
              "VERSION_MISMATCH",
              "SECTION_BUSY",
              "REPORT_IN_USE",
              "NOT_ALLOWED",
              "IN_USE",
              "REPORT_IN_TRASH",
              "REPORT_NOT_IN_TRASH",
              "WIDGET_EXECUTION_FAILED",
              "ROW_RULE",
              "POSITION_CHANGED",
              "SECTION_CHANGED",
              "AI_SUMMARY_NOT_ALLOWED",
              "DATA_VIEW_CHANGED",
              "WIDGET_CHANGED",
              "WIDGET_NOT_EDITABLE",
              "SCHEDULE_ALREADY_EXISTS",
              "ALREADY_QUEUED",
              "SCHEDULE_ALREADY_QUEUED",
              "SCHEDULE_QUEUED",
              "SCHEDULE_USES_SYSTEM_TEMPLATE",
              "NOT_EDITABLE_BY_API",
              "PORTAL_NOT_CONFIGURED",
              "USER_EMAIL_EXISTS",
              "ACCOUNT_HAS_NO_OWNER",
              "EMAIL_QUOTA_REACHED",
              "NOTIFICATION_FAILED",
              "INTERNAL"
            ]
          },
          "message": {
            "type": "string",
            "description": "Human-readable error description. Useful for logs and UIs; not stable across versions -- use `error_code` for programmatic handling."
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta",
            "description": "The base fields, plus the two replay fields when this error is the stored answer of an earlier write being replayed under the same idempotency_key."
          },
          "details": {
            "type": "object",
            "description": "Machine-readable detail of the refusal, present only on the errors that have one. Four shapes are published today. VALIDATION_FAILED and UNKNOWN_PARAM carry `problems`, an array of `Problem` naming the path, the code and the message of each thing that is wrong, with `suggestions` when a closed list exists; a body wrong in more than 50 places gets the first 50 and a last entry with code TOO_MANY_PROBLEMS. VERSION_MISMATCH carries `expected` and `actual`. REPORT_IN_USE carries `portal_views` and `reports`, each an array of {key, name} that must be detached first, and an entry of `reports` that is itself in the trash adds `in_trash: true`. REPORT_IN_TRASH carries `trashed_at`, `restorable_until` and `restore`. APPEND_NOT_ALLOWED and PORTAL_NOT_CONFIGURED carry `reason`, the INVALID_VALUE of a widget placement carries `first_free_row`, WIDGET_EXECUTION_FAILED carries the `verdict`, `message` and `reason` of the run, and ALREADY_QUEUED carries `schedule_key` and `report_key`: the one-off request of the snapshot that is still waiting, and the report it was asked of. IN_USE carries `used_by`, naming everything of the account that still holds the row a deletion was asked of, in the shape of that entity: an `EntityUsage` for a saved filter and a calculated metric, an `EntityUsageReports` for a language file and an `EntityUsageSchedules` for an email template. The IN_USE of the email template the client portal sends its notifications with carries `reason` `client_portal` as well, and it is the only IN_USE that carries a reason. NOT_ALLOWED carries `reason`, the rule that stops the write (`system_defined` for a row the platform ships and keeps).",
            "additionalProperties": true,
            "properties": {
              "problems": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Problem"
                }
              },
              "used_by": {
                "description": "What still reads the definition the call tried to delete. The families depend on the entity: a saved filter or a calculated metric is asked about reports, filtered metrics, portal views and Data Studio account groups; an email template about schedules and the client portal; a language file about reports.",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/EntityUsage"
                  },
                  {
                    "$ref": "#/components/schemas/EntityUsageSchedules"
                  },
                  {
                    "$ref": "#/components/schemas/EntityUsageReports"
                  }
                ]
              },
              "reason": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "The rule behind the refusal, where it has a name: the system_defined of a NOT_ALLOWED, the template_based of an APPEND_NOT_ALLOWED, the reason a widget could not be run. Null when the refusal has no named rule."
              }
            }
          }
        }
      },
      "IntegrationDetailRequest": {
        "type": "object",
        "required": [
          "integration_id"
        ],
        "properties": {
          "integration_id": {
            "type": "string",
            "description": "Public integration id (e.g. facebook_ads)."
          }
        }
      },
      "Integration": {
        "type": "object",
        "required": [
          "id",
          "name",
          "category",
          "supports_custom_fields_per_account",
          "queryable"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "supports_custom_fields_per_account": {
            "type": "boolean"
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Integration-wide advisories that do not fit in filters/settings/data_views (e.g. data freshness windows, upstream API quotas, billing notes). Optional: only present when there are global notes to publish. Each entry is a short, audience-API sentence; the array is unordered and clients should treat it as a bag of standalone notes."
          },
          "data_views": {
            "anyOf": [
              {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/DataView"
                }
              },
              {
                "type": "null"
              }
            ],
            "description": "The data views of the integration, or null when it has none (facebook_ads, ga4, google_search_console, twitter_analytics and cross_platform)."
          },
          "settings": {
            "anyOf": [
              {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Setting"
                }
              },
              {
                "type": "null"
              }
            ],
            "description": "The settings the integration reads, or null when it reads none."
          },
          "filters": {
            "$ref": "#/components/schemas/FiltersDescriptor"
          },
          "queryable": {
            "type": "boolean",
            "description": "Whether /v1/query and /v1/query/async accept this integration. When false, everything else is available -- the integration can be connected, its fields and filters can be read and its widgets can be created -- and only reading its rows through the query endpoints is refused."
          },
          "not_queryable_reason": {
            "type": "string",
            "description": "Why the data of this integration cannot be read through the query endpoints. Present only when queryable is false."
          }
        }
      },
      "DataView": {
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "description": "A data view exposed by an integration. The full list of valid `id` values for a given integration is in the response of /v1/integrations/detail (data_views[]). Example for google_ads: 22 views including `customer`, `campaign`, `ad_group`, `keyword_view`, `search_term_view`, etc.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "description": "Public-facing summary explaining what this data_view returns. Short (1-2 sentence) hint for the API consumer about the underlying scope/endpoint and notable restrictions; does NOT replace the integration-level `filters.note` which covers global concerns (settings, prefix, custom fields, etc.). Always present in real responses (iter 24+); omitted from compact examples in this spec for readability but emitted by /v1/integrations/detail for every data_view."
          },
          "type": {
            "type": "string",
            "enum": [
              "campaign",
              "list"
            ],
            "description": "Mailchimp-only: indicates which entity (campaign|list) this data_view affines with. The API validates that data_view.type matches settings.entity for mailchimp queries. Not present for other integrations."
          }
        }
      },
      "Setting": {
        "type": "object",
        "description": "Per-request settings that customize the query semantics for a given integration. Behaviour: when a setting's `applies_to_data_views` includes the current data_view (or applies_to_data_views is absent, meaning 'always applies'), the setting is REQUIRED in /v1/query and a missing one answers HTTP 200 with error_code MISSING_REQUIRED_FIELD. The value is validated strictly against the declared `options[]` (or `options_by_data_view[<dv>]` when present), and a value outside them answers HTTP 200 with error_code INVALID_VALUE; so does sending a setting whose `applies_to_data_views` does not cover the data_view of the call. An unknown setting key answers HTTP 200 with error_code UNKNOWN_PARAM. Dynamic settings (`dynamic: true`) are also REQUIRED when they apply, but the value is pass-through (no closed-list validation); use the `recommended_value` if you do not want to discover an account-specific value via /v1/connections.",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "required": {
            "type": "boolean",
            "description": "When true, the setting must always be sent in /v1/query. Only one setting is declared required without a recommended_value: mailchimp.entity."
          },
          "dynamic": {
            "type": "boolean",
            "description": "When true, the valid values are account-specific (discover via /v1/connections). The API does not validate the value against a closed list, but the setting is still REQUIRED when its applies_to_data_views matches."
          },
          "multi_select": {
            "type": "boolean",
            "description": "When true, the value is a JSON array of strings (each validated against `options[]`)."
          },
          "note": {
            "type": "string"
          },
          "applies_to_data_views": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "When present, the setting is REQUIRED only when the current data_view is in this list. When absent, the setting applies to every data_view of the integration."
          },
          "recommended_value": {
            "description": "A value the upstream native API would typically apply when this setting is not explicitly customized. PROVIDED ONLY AS A RECOMMENDATION for clients that do not have a specific preference. The API does NOT inject this value automatically: the client must always include the setting in /v1/query when it applies (per `applies_to_data_views`). Reporting Ninja does not define the recommended_value; it mirrors the upstream API's documented default behavior.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          },
          "options": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "value",
                "label"
              ],
              "properties": {
                "value": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                }
              }
            }
          },
          "options_by_data_view": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "object",
                "required": [
                  "value",
                  "label"
                ],
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  }
                }
              }
            },
            "description": "Per-data_view options map. Used when the valid values of a setting depend on the current data_view (e.g. pinterest_organic.pinterest_sort_by: 5 values for top_pins, 9 for top_video_pins)."
          }
        }
      },
      "FiltersDescriptor": {
        "type": "object",
        "required": [
          "supported"
        ],
        "description": "Describes the filter capabilities of an integration. Two shapes coexist: (a) integrations with a closed catalog of filter fields (e.g. facebook_ads, google_ads) expose `fields[]` with the operators valid for each filter field; (b) integrations with dynamic filter fields (e.g. ga4) expose `note`, `dimension_operators` and `metric_operators` and require the client to send `type_of_field` per filter.",
        "properties": {
          "supported": {
            "type": "boolean"
          },
          "combination": {
            "type": "string",
            "enum": [
              "and",
              "or",
              "and_or"
            ],
            "description": "How filters can be combined. `and` (facebook_ads, google_ads, microsoft_ads): all filters are AND-ed; the client must NOT send `combination` per filter (rejected for microsoft_ads). `and_or` (ga4): each filter may include `combination` (`AND` default, or `OR`); see ga4 notes for the iter 3 caveat about the second filter defining the group combination."
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FilterField"
            },
            "description": "Present for integrations with a closed catalog of filter fields."
          },
          "note": {
            "type": "string",
            "description": "Free-text guidance for the client: how filters can be combined, per-field caveats, matching rules for text operators, or the reason filters are not supported. Present for most integrations, and always worth reading before building a filter."
          },
          "dimension_operators": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Operators valid for dimension filters. Present for integrations with dynamic filter fields."
          },
          "metric_operators": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Operators valid for metric filters. Present for integrations with dynamic filter fields."
          }
        }
      },
      "FilterField": {
        "type": "object",
        "required": [
          "id",
          "name",
          "category",
          "operators"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "operators": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "allowed_values": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Closed list of valid filter values (case-sensitive). Present only for fields with a fixed enum (e.g. microsoft_ads CampaignStatus / AdDistribution / DeviceType). When present, every value sent in /v1/query.filters[].value MUST belong to this list (each element if value is an array)."
          },
          "data_views": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "List of data_view ids where this filter field is valid. Present for integrations where filter fields depend on the data_view (e.g. microsoft_ads). If absent, the field is valid for ALL data_views of the integration."
          },
          "aliases": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Other ids accepted for this same filter field, typically the id the field has in /v1/fields (e.g. facebook_ads `campaign_name` for the filter field `campaign.name`). Any of them may be sent as filters[].field and behaves exactly like the canonical id; `id` remains the canonical one and the one returned in this catalog. Present only for fields that have synonyms."
          },
          "type_of_field": {
            "type": "string",
            "description": "Whether the field is a dimension or a metric. Present for the integrations that ask the client to send type_of_field with each filter, and for the ones whose catalog states it field by field."
          }
        }
      },
      "ConnectionsRequest": {
        "type": "object",
        "required": [
          "integration_id"
        ],
        "properties": {
          "integration_id": {
            "type": "string",
            "description": "The integration whose connections are listed. One of the ids of /v1/integrations."
          },
          "settings": {
            "type": "object",
            "description": "Per-integration settings of this call. Only mailchimp reads one today; this endpoint does not refuse settings it does not read, it ignores them.",
            "properties": {
              "entity": {
                "type": "string",
                "description": "REQUIRED for mailchimp and ignored for every other integration: which of the two Mailchimp levels the connections are listed for. There is no default.",
                "enum": [
                  "campaign",
                  "list"
                ]
              }
            },
            "additionalProperties": true
          }
        }
      },
      "Connection": {
        "type": "object",
        "required": [
          "connection_key",
          "connection_name",
          "status"
        ],
        "properties": {
          "connection_key": {
            "type": "string"
          },
          "connection_name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "inactive"
            ],
            "description": "Connection status derived from the LAST KNOWN state of the underlying OAuth credentials. `active` = credentials were OK at the last refresh and queries are EXPECTED to succeed. `inactive` = credentials are known to be revoked, expired, in error state, pending re-authorization, or otherwise non-operational. For `cross_platform`, `active` requires ALL referenced credentials in the accounts group to be OK; if ANY of them is not OK (or has been deleted), the group reports `inactive`. IMPORTANT: this field reflects the state as last observed by Reporting Ninja, NOT a real-time check against the external provider. After a connection is created, the transition from `active` to `inactive` happens ONLY when the user explicitly refreshes the connection in the data sources view of the Reporting Ninja UI and the refresh fails. As a consequence, a connection reported here as `active` may in practice already be unusable (for example, because the upstream OAuth grant has expired or been revoked by the user on the provider side) without that having been detected yet -- in such cases a /v1/query call will return an authorization-related error. Treat `active` as a positive but non-binding signal, not a guarantee."
          },
          "accounts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Account"
            }
          },
          "analytics_views": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "description": "hubspot only: the analytics views of the connection, as the values its `analytics_view` setting takes."
          },
          "pipelines_stages": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "description": "hubspot only: the pipelines and their stages, as the values its `pipeline` and `stage` settings take."
          },
          "owners": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "description": "hubspot only: the owners of the portal, as the values its `owner` setting takes."
          }
        }
      },
      "Account": {
        "type": "object",
        "required": [
          "account_id",
          "account_name"
        ],
        "properties": {
          "account_id": {
            "type": "string"
          },
          "account_name": {
            "type": "string"
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 currency code for monetary metrics of this account. Provided by the advertising integrations whose native APIs expose a per-account currency (facebook_ads, google_ads, microsoft_ads, linkedin_ads, tiktok_ads, pinterest_ads, snapchat_ads, reddit_ads, twitter_ads), by ga4 (per-property currency), by klaviyo (account preferred currency, when the account declares one), by shopify (store currency) and by chatgpt_ads (ad account currency). OMITTED for integrations without monetary metrics (organic social, mailchimp, search_console, business_profile) and for those without a native per-account currency (hubspot, cross_platform). Clients must NOT assume a default currency for omitted cases."
          }
        }
      },
      "FieldsRequest": {
        "type": "object",
        "required": [
          "integration_id"
        ],
        "properties": {
          "integration_id": {
            "type": "string"
          },
          "connection_key": {
            "type": "string",
            "description": "REQUIRED when include_custom_fields is true. Also REQUIRED, with include_custom_fields false or absent, for hubspot with data_view contacts or deals, whose field list is always account specific. MUST NOT be present in any other case, nor for integrations whose supports_custom_fields_per_account is false. For cross_platform, account_id MUST equal connection_key (both are the key of the accounts group)."
          },
          "account_id": {
            "type": "string",
            "description": "REQUIRED when include_custom_fields is true. Also REQUIRED, with include_custom_fields false or absent, for hubspot with data_view contacts or deals, whose field list is always account specific. MUST NOT be present in any other case, nor for integrations whose supports_custom_fields_per_account is false. For cross_platform, account_id MUST equal connection_key (both are the key of the accounts group)."
          },
          "data_view": {
            "type": "string",
            "description": "Required when the integration exposes data_views. For example, REQUIRED for `google_ads` (one of its 22 data_views: `customer`, `campaign`, `ad_group`, `keyword_view`, etc.). NOT applicable to `facebook_ads`, `ga4`, `google_search_console`, `twitter_analytics` or `cross_platform`. Consult /v1/integrations/detail for the full list of valid values per integration."
          },
          "include_custom_fields": {
            "type": "boolean",
            "default": false,
            "description": "Whether to include account-specific custom fields (custom conversions for facebook_ads, custom dimensions/metrics for ga4, the campaign name dimensions of the accounts group for cross_platform). Default false. Only allowed when the integration's catalog declares supports_custom_fields_per_account=true. When true, both connection_key and account_id are REQUIRED. When false or absent, connection_key and account_id MUST NOT be present in the request. The deprecated key `include_custom_conversions` is rejected with an error -- use `include_custom_fields` instead."
          },
          "settings": {
            "type": "object",
            "description": "Per-integration settings of this call. Only mailchimp and amazon_ads read one. For amazon_ads an unknown key answers UNKNOWN_PARAM, and creative_type and tactics are rejected here: they change what /v1/query reads, not which fields exist.",
            "properties": {
              "entity": {
                "type": "string",
                "description": "REQUIRED for mailchimp: which of the two Mailchimp levels the fields belong to. It must agree with data_view (campaign_* with campaign, list_* with list).",
                "enum": [
                  "campaign",
                  "list"
                ]
              },
              "ad_types": {
                "type": "string",
                "description": "Optional for amazon_ads: a comma separated list of ad types (sponsored_products, sponsored_brands, sponsored_display) whose fields are returned. Each one must be offered by the data_view of the call. When absent, the fields of every ad type of that data_view are returned."
              }
            },
            "additionalProperties": true
          }
        }
      },
      "Field": {
        "type": "object",
        "required": [
          "field_id",
          "field_name",
          "field_type",
          "dim_met"
        ],
        "properties": {
          "field_id": {
            "type": "string"
          },
          "field_name": {
            "type": "string"
          },
          "field_description": {
            "type": "string"
          },
          "field_type": {
            "type": "string",
            "enum": [
              "text",
              "number",
              "currency",
              "percent",
              "date",
              "date_month",
              "url",
              "country",
              "region",
              "hour"
            ],
            "description": "Public field type. NOTE for ga4 temporal dimensions (date, week, month): the value format in /v1/query responses depends on the query. (a) If one of them is the only dimension of the query (\"trend\" mode), values come back with separators: `date` as YYYY-MM-DD (e.g. 2026-05-10), `month` as YYYY-MM (e.g. 2026-05), `week` as a YYYY-MM-DD date. (b) If the query has any other dimension (non-trend), values come back in raw GA4 format with NO year prefix for week/month: `date` as YYYYMMDD (e.g. 20260512), `month` as MM (e.g. 05), `week` as WW (e.g. 20). NOTE for percent metrics (CTR, conversion rate, engagement rate, bounce rate, etc.): values come back in 0-100 scale (e.g. `1.22` means 1.22%). Divide by 100 client-side if a 0-1 representation is needed."
          },
          "dim_met": {
            "type": "string",
            "enum": [
              "dimension",
              "metric"
            ]
          }
        }
      },
      "QueryRequest": {
        "type": "object",
        "required": [
          "integration_id",
          "connection_key",
          "account_id",
          "fields",
          "date_range",
          "limit"
        ],
        "properties": {
          "integration_id": {
            "type": "string"
          },
          "connection_key": {
            "type": "string"
          },
          "account_id": {
            "type": "string"
          },
          "data_view": {
            "type": "string"
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 1
          },
          "date_range": {
            "$ref": "#/components/schemas/DateRange"
          },
          "compare_to": {
            "$ref": "#/components/schemas/CompareRange"
          },
          "comparison_type": {
            "type": "string",
            "enum": [
              "percentage",
              "absolute",
              "value"
            ],
            "default": "percentage"
          },
          "settings": {
            "type": "object",
            "additionalProperties": true,
            "description": "Per-request settings. Validated strictly per integration: only keys declared in /v1/integrations/detail are accepted, and any other key answers HTTP 200 with error_code UNKNOWN_PARAM. Each setting that applies (per its applies_to_data_views) is REQUIRED and is not filled in for you; a missing one answers HTTP 200 with error_code MISSING_REQUIRED_FIELD. See the `Setting` schema and the catalog of each integration."
          },
          "filters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Filter"
            }
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50000,
            "description": "How many rows to bring back. A value of zero or less is refused with INVALID_LIMIT. A larger value is capped at 50000 instead of being refused."
          },
          "cursor": {
            "description": "Opaque cursor returned by a previous response as meta.next_cursor. Submit verbatim to continue pagination. Cursors expire 3 hours after the original query; an expired cursor answers CURSOR_EXPIRED. An unreadable cursor (different query or account) is silently ignored and pagination restarts from page 1. For async queries the same 3-hour expiration applies, even if the job result is still available.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "bypass_cache": {
            "type": "boolean",
            "default": false,
            "description": "If true, the API skips the cache lookup and always re-executes the query against the upstream platform. The fresh result is still written to the cache (so subsequent queries with the same input benefit). Useful when the client needs guaranteed-fresh data. Applies to both /v1/query (sync) and /v1/query/async."
          }
        }
      },
      "DateRange": {
        "type": "object",
        "required": [
          "preset"
        ],
        "properties": {
          "preset": {
            "type": "string",
            "enum": [
              "today",
              "yesterday",
              "lastweeksunsat",
              "lastweekmonsun",
              "thismonth",
              "thismonthtoyesterday",
              "lastmonth",
              "thisyear",
              "lastyear",
              "lastyeartodate",
              "lastxdays",
              "lastxweeks",
              "lastxweeksiso",
              "lastxmonths",
              "lastxyears",
              "custom"
            ]
          },
          "x": {
            "type": "integer",
            "description": "Required for lastx* presets."
          },
          "include_current": {
            "type": "boolean",
            "description": "Optional for lastxmonths."
          },
          "start": {
            "type": "string",
            "description": "Required for custom. Written as yyyy-MM-dd. Only the shape is checked: a date that does not exist, or an end before the start, is passed on to the integration as it is."
          },
          "end": {
            "type": "string",
            "description": "Required for custom. Written as yyyy-MM-dd. Only the shape is checked: a date that does not exist, or an end before the start, is passed on to the integration as it is."
          }
        }
      },
      "CompareRange": {
        "type": "object",
        "required": [
          "preset"
        ],
        "properties": {
          "preset": {
            "type": "string",
            "enum": [
              "none",
              "previous",
              "year_ago",
              "year_ago_match_weekday",
              "custom"
            ]
          },
          "start": {
            "type": "string",
            "format": "date",
            "description": "Required for custom."
          },
          "end": {
            "type": "string",
            "format": "date",
            "description": "Required for custom."
          }
        }
      },
      "Filter": {
        "type": "object",
        "required": [
          "field",
          "operator",
          "value"
        ],
        "properties": {
          "field": {
            "type": "string",
            "description": "Filter field id from /v1/integrations/detail or /v1/fields. The authoritative list is filters.fields[] in /v1/integrations/detail: a field of /v1/fields is filterable only if it appears there, under that id or among its aliases. Two things follow for facebook_ads. The campaign, ad set and ad names and ids are `campaign_name`, `campaign_id`, `adset_name`, `adset_id`, `ad_name` and `ad_id` in /v1/fields and `campaign.name`, `campaign.id`, `adset.name`, `adset.id`, `ad.name` and `ad.id` as filter fields, and both forms are accepted (the synonyms are published in filters.fields[].aliases). And a few fields are filterable only in a different shape, with no alias possible: the single `effective_status` field of /v1/fields is filtered per level (`campaign.effective_status`, `adset.effective_status`, `ad.effective_status`), and the metrics you request in fields[] (`impressions`, `clicks`, `spend`) are not filter fields themselves, the filterable ones being the metrics of each entity (`campaign.impressions`, `ad.clicks`, `adset.lifetime_spent` and so on). For facebook_ads, the audience segment filter (user_segment_key) is only accepted when the same query requests the user_segment_key dimension, and it cannot be combined with the publisher_platform, device_platform or impression_device filters."
          },
          "operator": {
            "type": "string",
            "description": "Public operator. The valid set depends on the integration and the filter field: see /v1/integrations/detail.filters.fields[].operators per filter field. Exception: ga4 declares its operator sets in /v1/integrations/detail.filters.dimension_operators or metric_operators depending on the filter's type_of_field."
          },
          "value": {
            "description": "Scalar or array depending on the operator. For ga4 IN_LIST/NOT_IN_LIST, google_ads IN/NOT_IN and klaviyo/shopify/chatgpt_ads IN/NOT_IN arrays are accepted (klaviyo/shopify/chatgpt_ads IN/NOT_IN also accept a comma-separated string); for scalar operators arrays are rejected. For metric filters in ga4 the value must be numeric."
          },
          "type_of_field": {
            "type": "string",
            "enum": [
              "dimension",
              "metric"
            ],
            "description": "Required by integrations whose filter catalog declares it (e.g. ga4 -- dimension or metric; and linkedin_ads/tiktok_ads/pinterest_ads/snapchat_ads/reddit_ads/klaviyo/shopify/chatgpt_ads -- dimension). Declares whether the field is a dimension or a metric so the right operator set is validated. Other integrations derive it internally and do not accept it; see /v1/integrations/detail for the rule per integration."
          },
          "combination": {
            "type": "string",
            "enum": [
              "AND",
              "OR"
            ],
            "description": "Optional combination of this filter with the previous one. Default \"AND\" (for shopify and chatgpt_ads, \"OR\"). OR support varies by integration: ga4 accepts AND and OR; shopify and chatgpt_ads accept AND and OR but require the same combination on every condition (mixes are rejected); linkedin_ads/tiktok_ads/pinterest_ads/snapchat_ads/reddit_ads/klaviyo are OR-only (AND is rejected); facebook_ads, google_ads and the rest force AND. See /v1/integrations/detail for the rule per integration. When 2+ filters are sent, the combination of the second filter defines the combination of the whole group (for shopify and chatgpt_ads, mixed combinations are rejected instead)."
          }
        }
      },
      "AsyncQueryAcceptedResponse": {
        "type": "object",
        "description": "Envelope returned by POST /v1/query/async. The `data.job_id` is the opaque identifier to pass to /v1/query/status when polling for the result.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "type": "object",
            "required": [
              "job_id",
              "status"
            ],
            "properties": {
              "job_id": {
                "type": "string",
                "description": "Opaque identifier of the persisted async query job. Pass this to /v1/query/status to poll for the result.",
                "example": "1234567890"
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending"
                ],
                "description": "Initial state of the job after submission. Transitions to running -> success/error as the executor processes it."
              }
            }
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "AsyncQueryStatusResponse": {
        "type": "object",
        "description": "Envelope returned by POST /v1/query/status. When `data.status` is `success`, `data.result` embeds the same `{status, data.rows[], meta}` body that /v1/query would have produced. When `data.status` is `error`, `data.error_message` describes the failure.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "type": "object",
            "required": [
              "job_id",
              "status"
            ],
            "properties": {
              "job_id": {
                "type": "string",
                "example": "1234567890"
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "running",
                  "success",
                  "error"
                ],
                "description": "Current state. `pending`: awaiting processing. `running`: executing. `success`: completed; see data.result. `error`: completed with failure; see data.error_message."
              },
              "created_at": {
                "type": "integer",
                "format": "int64",
                "description": "UNIX epoch milliseconds when the job was submitted."
              },
              "started_at": {
                "type": "integer",
                "format": "int64",
                "description": "UNIX epoch milliseconds when the executor started. Only present when status is running/success/error."
              },
              "completed_at": {
                "type": "integer",
                "format": "int64",
                "description": "UNIX epoch milliseconds when the executor completed (with success or error). Only present when status is success/error."
              },
              "result": {
                "type": "object",
                "description": "Embedded /v1/query response body (status + data.rows[] + meta). Same shape as the synchronous /v1/query response. Only present when status is success."
              },
              "error_message": {
                "type": "string",
                "description": "Truncated error description (<= 500 chars). Only present when status is error."
              }
            }
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "One thing the API has to say about a field of a request. Problems travel inside details.problems of a VALIDATION_FAILED or an UNKNOWN_PARAM and inside the `problems` of an invalid dry run; the same shape carries the `warnings[]` of a write that DID happen, where the code is one of the four that are not refusals at all.",
        "required": [
          "path",
          "code",
          "message"
        ],
        "properties": {
          "path": {
            "type": "string",
            "description": "Dotted path of the offending field inside the request body, empty for a problem about the body as a whole."
          },
          "code": {
            "type": "string",
            "description": "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.",
            "enum": [
              "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"
            ]
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation. Not stable across versions."
          },
          "suggestions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The accepted values, when the field takes a closed list."
          }
        }
      },
      "QueryMeta": {
        "type": "object",
        "description": "The meta of a query answer: the base fields plus what the query resolved and how much of it came back.",
        "required": [
          "request_id",
          "timestamp",
          "total_rows",
          "returned_rows",
          "has_more",
          "date_range_resolved"
        ],
        "properties": {
          "request_id": {
            "type": "string"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "total_rows": {
            "type": "integer",
            "description": "Rows the query matched."
          },
          "returned_rows": {
            "type": "integer",
            "description": "Rows in this page."
          },
          "has_more": {
            "type": "boolean",
            "description": "Whether another page is available."
          },
          "next_cursor": {
            "type": "string",
            "description": "Cursor for the next page. Present only when has_more is true."
          },
          "date_range_resolved": {
            "type": "object",
            "description": "The period the preset resolved to, as yyyy-MM-dd.",
            "required": [
              "start",
              "end"
            ],
            "properties": {
              "start": {
                "type": "string"
              },
              "end": {
                "type": "string"
              }
            }
          },
          "compare_date_range_resolved": {
            "type": "object",
            "description": "The comparison period. Present only when compare_to was sent.",
            "required": [
              "start",
              "end"
            ],
            "properties": {
              "start": {
                "type": "string"
              },
              "end": {
                "type": "string"
              }
            }
          },
          "comparison_type": {
            "type": "string",
            "description": "How the comparison columns are expressed. Present only when compare_to was sent.",
            "enum": [
              "percentage",
              "absolute",
              "value"
            ]
          }
        }
      },
      "WriteMeta": {
        "type": "object",
        "description": "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.",
        "required": [
          "request_id",
          "timestamp"
        ],
        "properties": {
          "request_id": {
            "type": "string",
            "description": "Id of THIS request, including on a replay."
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "replayed": {
            "type": "boolean",
            "description": "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": {
            "description": "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.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "Verification": {
        "type": "object",
        "description": "The verdict of a verify_data run.",
        "required": [
          "verdict"
        ],
        "properties": {
          "verdict": {
            "type": "string",
            "description": "What happened when the widget was run against the provider. `success`: rows came back. `success_empty`: the provider answered with no rows. `not_verifiable`: it could not be run right now. `failure`: the provider refused it, and the widget is not written.",
            "enum": [
              "success",
              "success_empty",
              "not_verifiable",
              "failure"
            ]
          },
          "message": {
            "description": "What the provider said, when it said anything.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "reason": {
            "description": "Why the run did not happen, for not_verifiable. `busy` means the verification capacity is in use and the call did not wait for it.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "WriteDryRunData": {
        "type": "object",
        "description": "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.",
        "required": [
          "dry_run",
          "valid"
        ],
        "properties": {
          "dry_run": {
            "type": "boolean",
            "description": "Always true in this shape.",
            "enum": [
              true
            ]
          },
          "valid": {
            "type": "boolean",
            "description": "Whether the body would be accepted."
          },
          "message": {
            "type": "string",
            "description": "Why the body would be refused. Present only when valid is false."
          },
          "problems": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Everything that is wrong with the body. Present only when valid is false."
          }
        },
        "additionalProperties": true
      },
      "WriteConfirmationRequiredData": {
        "type": "object",
        "description": "The answer of the first call to an endpoint that asks for a confirmation. Nothing was written. The fields of the endpoint's own preview travel alongside these three, so the caller can show what is about to happen. The token is good for one use, for this account, this endpoint and this exact body.",
        "required": [
          "confirmation_required",
          "confirmation_token",
          "expires_at"
        ],
        "properties": {
          "confirmation_required": {
            "type": "boolean",
            "description": "Always true in this shape.",
            "enum": [
              true
            ]
          },
          "confirmation_token": {
            "type": "string",
            "description": "Send the same body again with this token to perform the write."
          },
          "expires_at": {
            "type": "string",
            "description": "When the token stops being accepted, 10 minutes after it was issued.",
            "format": "date-time"
          }
        },
        "additionalProperties": true
      },
      "WidgetDsl": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "description": "Title of the widget, in the requested output language. Plain text, no markup. At most 80 characters."
          },
          "visualization": {
            "type": "string",
            "description": "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.",
            "enum": [
              "kpi",
              "graph_basic",
              "graph_pie",
              "graph_trend",
              "table_metrics_summary",
              "table_dimensions_metrics",
              "text"
            ]
          },
          "chart_type": {
            "description": "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.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "area",
                  "line",
                  "column",
                  "bar"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "full_width": {
            "description": "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.",
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "content": {
            "description": "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.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "data_view": {
            "type": "string",
            "description": "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": {
            "type": "array",
            "description": "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.",
            "items": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Public field id of the metric, the same one the short form carries."
                    },
                    "filter_key": {
                      "description": "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.",
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  },
                  "required": [
                    "id",
                    "filter_key"
                  ]
                }
              ]
            }
          },
          "dimensions": {
            "type": "array",
            "description": "Public field ids of the dimensions, in the order they are shown. Empty when the widget has none.",
            "items": {
              "type": "string"
            }
          },
          "filter_key": {
            "description": "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.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "compare": {
            "description": "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.",
            "anyOf": [
              {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "by": {
                    "type": "string",
                    "description": "What the widget compares.",
                    "enum": [
                      "accounts",
                      "filters",
                      "breakdown"
                    ]
                  },
                  "items": {
                    "description": "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.",
                    "anyOf": [
                      {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "additionalProperties": false,
                          "properties": {
                            "connection_key": {
                              "type": "string",
                              "description": "Connection the account is read through, as the list of connections publishes it."
                            },
                            "account_id": {
                              "type": "string",
                              "description": "Account of that connection, as the list of connections publishes it."
                            },
                            "label": {
                              "description": "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.",
                              "anyOf": [
                                {
                                  "type": "string"
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            },
                            "filter_key": {
                              "description": "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.",
                              "anyOf": [
                                {
                                  "type": "string"
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            }
                          },
                          "required": [
                            "connection_key",
                            "account_id",
                            "label",
                            "filter_key"
                          ]
                        }
                      },
                      {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "additionalProperties": false,
                          "properties": {
                            "filter_key": {
                              "type": "string",
                              "description": "Key of a saved filter of the account, as the list of filters publishes it. The same filter is never compared twice."
                            }
                          },
                          "required": [
                            "filter_key"
                          ]
                        }
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "over_time": {
                    "description": "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.",
                    "anyOf": [
                      {
                        "type": "string",
                        "enum": [
                          "none",
                          "hour_of_day",
                          "day_of_week",
                          "date",
                          "week",
                          "month",
                          "year"
                        ]
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "show_past_serie": {
                    "description": "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.",
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                },
                "required": [
                  "by",
                  "items",
                  "over_time",
                  "show_past_serie"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "filters": {
            "description": "Filter applied to the widget, or null for no filter.",
            "anyOf": [
              {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "combination": {
                    "type": "string",
                    "description": "How the conditions are joined. Only a combination the catalogue declares for this integration.",
                    "enum": [
                      "and",
                      "or"
                    ]
                  },
                  "conditions": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "object",
                      "additionalProperties": false,
                      "properties": {
                        "field": {
                          "type": "string",
                          "description": "Public id of a filter field of the catalogue."
                        },
                        "operator": {
                          "type": "string",
                          "description": "Operator declared by the catalogue for that field or category."
                        },
                        "value": {
                          "type": "string",
                          "description": "Value to compare against, always as text: digits for numbers, comma separated for list operators."
                        },
                        "type_of_field": {
                          "description": "dimension or metric, only when the catalogue says this integration requires it; null otherwise.",
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        }
                      },
                      "required": [
                        "field",
                        "operator",
                        "value",
                        "type_of_field"
                      ]
                    }
                  }
                },
                "required": [
                  "combination",
                  "conditions"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "sort_by": {
            "description": "Ordering of the rows, or null to leave the default.",
            "anyOf": [
              {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "field": {
                    "type": "string",
                    "description": "Public field id, present in metrics or dimensions."
                  },
                  "direction": {
                    "type": "string",
                    "enum": [
                      "asc",
                      "desc"
                    ]
                  }
                },
                "required": [
                  "field",
                  "direction"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "limit": {
            "description": "Maximum number of rows, or null for the default.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "date_range": {
            "description": "Date range of the widget, or null to inherit the range of the report section.",
            "anyOf": [
              {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "preset": {
                    "type": "string",
                    "description": "One of the preset ids listed in the instructions, or custom when start and end are given."
                  },
                  "x": {
                    "description": "Number of days, weeks or months of a preset that takes one; null otherwise.",
                    "anyOf": [
                      {
                        "type": "integer"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "start": {
                    "description": "First day of a custom range as yyyy-MM-dd; null otherwise.",
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "end": {
                    "description": "Last day of a custom range as yyyy-MM-dd; null otherwise.",
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                },
                "required": [
                  "preset",
                  "x",
                  "start",
                  "end"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "previous_period": {
            "description": "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.",
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "show_change": {
            "description": "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.",
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "settings": {
            "description": "Integration settings the catalogue declares, or null when none applies.",
            "anyOf": [
              {
                "type": "array",
                "minItems": 1,
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Setting id of the catalogue."
                    },
                    "value": {
                      "type": "string",
                      "description": "One of the values the catalogue lists for that setting."
                    }
                  },
                  "required": [
                    "id",
                    "value"
                  ]
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "unfulfilled": {
            "type": "array",
            "description": "What was asked for and cannot be expressed with this integration, one short sentence each. Empty when everything asked for is in the widget.",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "name",
          "visualization",
          "chart_type",
          "full_width",
          "content",
          "data_view",
          "metrics",
          "dimensions",
          "filter_key",
          "compare",
          "filters",
          "sort_by",
          "limit",
          "date_range",
          "previous_period",
          "show_change",
          "settings",
          "unfulfilled"
        ]
      },
      "ReportDataSource": {
        "type": "object",
        "description": "One report level binding: which connection and which account the report reads an integration from.",
        "required": [
          "integration_id",
          "connection_key",
          "account_id"
        ],
        "properties": {
          "integration_id": {
            "description": "Public id of the integration, or null when this source has no public id and cannot be addressed through the API.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "integration": {
            "type": "string",
            "description": "Internal name of the source. Present only when integration_id is null."
          },
          "connection_key": {
            "description": "The connection this report reads that integration from.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "account_id": {
            "description": "The account inside that connection.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "entity": {
            "type": "string",
            "description": "mailchimp only: which of its two levels this binding is for.",
            "enum": [
              "campaign",
              "list"
            ]
          }
        }
      },
      "ReportHeader": {
        "type": "object",
        "description": "What every listing and every read of a report says about it.",
        "required": [
          "report_key",
          "name",
          "internal_name",
          "internal_notes",
          "subtitle",
          "type",
          "based_on_template_key",
          "catalog",
          "created_via",
          "version",
          "created_at",
          "report_level_data_sources",
          "data_sources",
          "share_link_enabled",
          "share_link_id",
          "share_url",
          "app_url",
          "share_link_password_set",
          "share_link_allow_date_range"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The key that addresses this report in every other endpoint."
          },
          "name": {
            "description": "Title of the report: the name the cover paints, and the one every write takes as `name`. At most 60 characters.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The OTHER name of the report: the one the list of the application shows. A report has two and both are writable. At most 100 characters."
          },
          "subtitle": {
            "description": "The line under the title of the report, as /v1/reports/update writes it. Null when the report has none.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "description": "The note the account keeps about this report. It is never shown to a client of the account: it belongs to whoever builds the report.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "type": {
            "type": "string",
            "description": "`report` is a normal report, `template` one of the account's own templates, `template_based` a report whose sections are its template's.",
            "enum": [
              "report",
              "template",
              "template_based"
            ]
          },
          "based_on_template_key": {
            "description": "The template a template_based report follows. Null for the other two types.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "catalog": {
            "description": "The catalog entry this report was materialized from, or null when it was not.",
            "anyOf": [
              {
                "type": "object",
                "required": [
                  "slug",
                  "version"
                ],
                "properties": {
                  "slug": {
                    "type": "string"
                  },
                  "version": {
                    "anyOf": [
                      {
                        "type": "integer"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "created_via": {
            "type": "string",
            "description": "Whether the report was created through this API or in the app.",
            "enum": [
              "api",
              "ui"
            ]
          },
          "version": {
            "type": "integer",
            "description": "Version of the sections structure. It is what if_version compares against, and it moves only when the list of sections changes."
          },
          "created_at": {
            "description": "When the report was created, in UTC.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_level_data_sources": {
            "type": "boolean",
            "description": "True when every widget reads from the bindings of data_sources; false when each widget carries its own connection, which is a report the API cannot extend."
          },
          "data_sources": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReportDataSource"
            },
            "description": "The report level bindings, ordered by integration. A binding without an account is not published."
          },
          "share_link_enabled": {
            "type": "boolean",
            "description": "Whether the public share link of the report is on."
          },
          "share_link_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The id in the public /share URL, or null when the report has never been shared."
          },
          "share_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The public URL of the report, built on the account's own portal domain when it has one. Null when the report has no link."
          },
          "app_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The link that opens this report in the Reporting Ninja application. Opening it requires a signed-in user of the account, so it is not the public link: `share_url` is the one anybody holding it reads. Null for a report in the trash, which the application does not open."
          },
          "share_link_password_set": {
            "type": "boolean",
            "description": "Whether the link asks for a password. The password itself is never published."
          },
          "share_link_allow_date_range": {
            "type": "boolean",
            "description": "Whether whoever opens the link may change the period."
          },
          "settings": {
            "$ref": "#/components/schemas/ReportSettings",
            "description": "The settings window of the report. Present in the answer of /v1/reports/get and of /v1/reports/update, which are about one report; the listings leave it out."
          }
        }
      },
      "ReportWidget": {
        "type": "object",
        "description": "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.",
        "required": [
          "widget_key",
          "name",
          "row",
          "column",
          "type",
          "integration_id",
          "data_view",
          "editable"
        ],
        "properties": {
          "widget_key": {
            "type": "string"
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "row": {
            "description": "1-based row of the widget inside its section.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "column": {
            "description": "1-based column of the widget inside its row.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "type": {
            "type": "string",
            "description": "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.",
            "enum": [
              "text",
              "ai_summary",
              "image",
              "page_break",
              "kpi",
              "metrics_summary",
              "trend",
              "chart",
              "table",
              "comparator",
              "other"
            ]
          },
          "integration_id": {
            "description": "Public id of the integration, or null when this source has no public id and cannot be addressed through the API.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "integration": {
            "type": "string",
            "description": "Internal name of the source. Present only when integration_id is null."
          },
          "data_view": {
            "description": "The level the widget reads at, in the vocabulary /v1/capabilities publishes for that integration, or null when the widget has none.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "connection_key": {
            "description": "Present only when the report does NOT use report level data sources.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "account_id": {
            "description": "Present only when the report does NOT use report level data sources.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "editable": {
            "type": "boolean",
            "description": "Whether this widget can be expressed as a DSL definition."
          },
          "dsl": {
            "$ref": "#/components/schemas/WidgetDsl",
            "description": "What the widget shows. Present only when editable is true."
          },
          "reason": {
            "description": "Why the widget has no definition, as a stable token. Present only when editable is false.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "message": {
            "description": "The same reason in words. Present only when editable is false.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "ai_summary": {
            "$ref": "#/components/schemas/WidgetAiSummary",
            "description": "Present only for a widget of type ai_summary: what that summary reads, the settings it is generated with and the brief of this summary. It carries no definition, so `editable` is false for it and `dsl` is absent."
          },
          "image": {
            "allOf": [
              {
                "$ref": "#/components/schemas/WidgetImage"
              }
            ],
            "description": "Present only when type is image: the size of the picture and how much its file weighs."
          }
        }
      },
      "ReportSection": {
        "type": "object",
        "description": "One section of a report, with its widgets in the order they are laid out, and the two keys a section of a composition is written with, so a report that is read can be written back with the same vocabulary.",
        "required": [
          "section_key",
          "name",
          "show_previous_period",
          "add_summary",
          "show_date_range",
          "page_orientation",
          "page_break",
          "widgets"
        ],
        "properties": {
          "section_key": {
            "type": "string"
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "show_previous_period": {
            "type": "boolean",
            "description": "True when the header of the section names the period it is compared against, under its date range. The same key a section of /v1/reports/create takes."
          },
          "add_summary": {
            "description": "The AI summary OF THIS SECTION, said the way `add_summary` asks for one: `null` when the section carries none, `{\"position\":\"first\"}` or `{\"position\":\"last\"}` when it is in the first or the last row of the section, and `true` when it carries one placed by hand somewhere in between, which no position of the composition describes (writing that report back with `true` puts it in the first row). A summary of the WHOLE report hosted by this section is NOT answered here: it shows up as one more widget of type `ai_summary`, and the `add_summary` at the root of a composition is what asks for it.",
            "anyOf": [
              {
                "type": "null"
              },
              {
                "type": "boolean"
              },
              {
                "type": "object",
                "required": [
                  "position"
                ],
                "properties": {
                  "position": {
                    "type": "string",
                    "enum": [
                      "first",
                      "last"
                    ]
                  }
                },
                "additionalProperties": false
              }
            ]
          },
          "widgets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReportWidget"
            }
          },
          "show_date_range": {
            "type": "boolean",
            "description": "Whether the header of the section shows the period it covers."
          },
          "page_orientation": {
            "description": "How the pages of this section are laid out in the PDF, or null when the section holds no value for it.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "portrait",
                  "landscape"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "page_break": {
            "type": "boolean",
            "description": "Whether the PDF starts a new page at this section."
          }
        }
      },
      "WrittenWidget": {
        "type": "object",
        "description": "One widget as a write answers it: enough to address it, without its definition.",
        "required": [
          "widget_key",
          "name",
          "row",
          "column"
        ],
        "properties": {
          "widget_key": {
            "type": "string"
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "row": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "column": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "integration_id": {
            "description": "Public id of the integration, or null when this source has no public id and cannot be addressed through the API.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "integration": {
            "type": "string",
            "description": "Internal name of the source. Present only when integration_id is null."
          }
        }
      },
      "WrittenSection": {
        "type": "object",
        "description": "One section as a write answers it.",
        "required": [
          "section_key",
          "name",
          "origin",
          "widgets"
        ],
        "properties": {
          "section_key": {
            "type": "string"
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "origin": {
            "type": "string",
            "description": "Where the section came from: copied from the catalog, written from the widgets of the request, or the AI summary section.",
            "enum": [
              "catalog",
              "custom",
              "summary"
            ]
          },
          "widgets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WrittenWidget"
            }
          }
        }
      },
      "Schedule": {
        "type": "object",
        "description": "One scheduled delivery of a report by email.",
        "required": [
          "schedule_key",
          "report_key",
          "frequency",
          "recipients_to",
          "recipients_cc",
          "recipients_bcc",
          "uses_system_template"
        ],
        "properties": {
          "schedule_key": {
            "description": "The key that addresses this schedule.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_key": {
            "description": "The report that is delivered.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "frequency": {
            "description": "How often the report is sent. `none` is a schedule that is kept and does not deliver; `now` appears only in the answer of a one-off delivery.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "daily",
                  "weekly",
                  "monthly",
                  "none",
                  "now"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "frequency_details": {
            "description": "The weekday in upper case for `weekly`, and the day of the month as a string from \"1\" to \"31\" for `monthly`.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "time": {
            "description": "Hour of the delivery in the time zone of the account, as HH:00.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "recipients_to": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "recipients_cc": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "recipients_bcc": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "report_format": {
            "description": "How the report travels: attached as a PDF, attached as a spreadsheet, or as a link in the body.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "pdf",
                  "xls",
                  "link"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "email_template_key": {
            "description": "The email template of the account this delivery uses, or null when it uses none and when it uses the system one.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "uses_system_template": {
            "type": "boolean",
            "description": "True when the delivery is built from the system template in the language of the user, which this version cannot express and cannot update."
          },
          "format": {
            "description": "Body of the email.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "html",
                  "plain_text"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "subject": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "sender": {
            "description": "Display name of the sender.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "sender_email": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "reply_to_email": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "origin": {
            "description": "Where the schedule was created.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "app",
                  "portal",
                  "api"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "user_key": {
            "description": "The user the delivery is attributed to.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "conditional_execution_enabled": {
            "type": "boolean",
            "description": "True when the delivery only goes out if a condition holds. This version cannot express the condition, and refuses to change a schedule that has one."
          },
          "last_execution_at": {
            "description": "When it was last delivered, in UTC.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "queued": {
            "type": "boolean",
            "description": "True while this delivery is being sent. A schedule cannot be changed or stopped while it is true."
          }
        }
      },
      "PortalView": {
        "type": "object",
        "description": "One view of the client portal: which reports which client users see, and how.",
        "required": [
          "portal_view_key",
          "name",
          "send_notification",
          "allow_to_change_date_range",
          "report_keys",
          "user_keys"
        ],
        "properties": {
          "portal_view_key": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "description": "A note for the account's own staff. It is never shown in the portal.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "brand_template_key": {
            "description": "The brand the portal wears for these users, or null for the default brand of the account.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "send_notification": {
            "type": "boolean",
            "description": "Whether the users are emailed that the view is available."
          },
          "allow_to_change_date_range": {
            "type": "boolean",
            "description": "Whether the users may change the period of what they see."
          },
          "date_range_option": {
            "description": "Which periods the portal offers: every period, or only whole past months. These are the two values this version names; a view configured with a period choice added later is published with the value it is stored under, outside this list.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "all",
                  "last_monthly_periods"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "report_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The reports of the view, in the order of the portal menu."
          },
          "user_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The client users who see it."
          }
        }
      },
      "ClientUser": {
        "type": "object",
        "description": "One client user of the account: someone who signs into the client portal and sees the views he is in.",
        "required": [
          "user_key",
          "email",
          "first_name",
          "last_name",
          "language",
          "role",
          "created_at"
        ],
        "properties": {
          "user_key": {
            "type": "string"
          },
          "email": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "first_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "last_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "language": {
            "description": "Language of the portal and of the emails for this user.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "role": {
            "type": "string",
            "description": "Always `client`. The owner and the administrators of the account are not part of this surface.",
            "enum": [
              "client"
            ]
          },
          "created_at": {
            "description": "When the user was created, in UTC.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "CatalogTemplate": {
        "type": "object",
        "description": "One published template of the system catalog. Every account sees the same catalog.",
        "required": [
          "slug",
          "version",
          "name",
          "description",
          "type",
          "category",
          "required_integrations",
          "optional_integrations",
          "parameterized"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "description": "The id a report is created from."
          },
          "version": {
            "type": "integer",
            "description": "The published version of that entry. It is not sent when a section of it is used in a composition: the version published today is always the one taken."
          },
          "name": {
            "description": "In the requested language.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "description": {
            "description": "In the requested language.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "type": {
            "description": "How complete the entry is. These are the three kinds this version names; an entry of a kind added later is published with the value it is stored under, outside this list.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "essential",
                  "advanced",
                  "reference"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "category": {
            "description": "Lower case token of the category, spaces turned into underscores.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "required_integrations": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "integration_id": {
                  "description": "Public id of the integration, or null when this source has no public id and cannot be addressed through the API.",
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "integration": {
                  "type": "string",
                  "description": "Internal name of the source. Present only when integration_id is null."
                }
              }
            },
            "description": "The integrations the entry cannot be materialized without."
          },
          "optional_integrations": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "integration_id": {
                  "description": "Public id of the integration, or null when this source has no public id and cannot be addressed through the API.",
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "integration": {
                  "type": "string",
                  "description": "Internal name of the source. Present only when integration_id is null."
                }
              }
            },
            "description": "The integrations whose sections are skipped when they are not bound."
          },
          "parameterized": {
            "type": "boolean",
            "description": "True when the entry asks for values before it can be materialized. A parameterized entry cannot be used as a section of a composition."
          },
          "sections": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "position",
                "name",
                "integrations",
                "required"
              ],
              "properties": {
                "position": {
                  "type": "integer",
                  "description": "0-based position, which is what a catalog section of a composition names."
                },
                "name": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "integrations": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "integration_id": {
                        "description": "Public id of the integration, or null when this source has no public id and cannot be addressed through the API.",
                        "anyOf": [
                          {
                            "type": "string"
                          },
                          {
                            "type": "null"
                          }
                        ]
                      },
                      "integration": {
                        "type": "string",
                        "description": "Internal name of the source. Present only when integration_id is null."
                      }
                    }
                  }
                },
                "required": {
                  "type": "boolean"
                }
              }
            },
            "description": "Present only when include_sections was true."
          },
          "parameters": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "type",
                "integrations",
                "required"
              ],
              "properties": {
                "id": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "type": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "integrations": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "integration_id": {
                        "description": "Public id of the integration, or null when this source has no public id and cannot be addressed through the API.",
                        "anyOf": [
                          {
                            "type": "string"
                          },
                          {
                            "type": "null"
                          }
                        ]
                      },
                      "integration": {
                        "type": "string",
                        "description": "Internal name of the source. Present only when integration_id is null."
                      }
                    }
                  }
                },
                "required": {
                  "type": "boolean"
                }
              }
            },
            "description": "Present only when include_sections was true. The values a parameterized entry asks for."
          }
        }
      },
      "IntegrationsListData": {
        "type": "object",
        "description": "The catalog as a list. Only the identity fields of each integration travel here; the data views, the settings and the filter catalog are answered by /v1/integrations/detail.",
        "required": [
          "integrations"
        ],
        "properties": {
          "integrations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Integration"
            }
          }
        }
      },
      "IntegrationsListResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/IntegrationsListData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "IntegrationDetailData": {
        "type": "object",
        "description": "An array with the one integration that was asked for.",
        "required": [
          "integrations"
        ],
        "properties": {
          "integrations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Integration"
            }
          }
        }
      },
      "IntegrationDetailResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/IntegrationDetailData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "ConnectionsListData": {
        "type": "object",
        "description": "The authorized connections of the account for one integration, each with the accounts it reaches.",
        "required": [
          "connections"
        ],
        "properties": {
          "connections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Connection"
            }
          }
        }
      },
      "ConnectionsListResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/ConnectionsListData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "FieldsListData": {
        "type": "object",
        "description": "The dimensions and metrics available for one integration and data view.",
        "required": [
          "fields"
        ],
        "properties": {
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Field"
            }
          },
          "default_dimension": {
            "type": "string",
            "description": "The dimension the app picks when none is chosen. Present only when the catalog declares one."
          },
          "default_metric": {
            "type": "string",
            "description": "The metric the app picks when none is chosen. Present only when the catalog declares one."
          }
        }
      },
      "FieldsListResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/FieldsListData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "QueryData": {
        "type": "object",
        "description": "The rows of the page. How many there are in total, whether another page exists and which period the presets resolved to all travel in meta.",
        "required": [
          "rows"
        ],
        "properties": {
          "rows": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "description": "One object per row, keyed by the field ids that were asked for."
          }
        }
      },
      "QueryResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/QueryData"
          },
          "meta": {
            "$ref": "#/components/schemas/QueryMeta"
          }
        }
      },
      "QueryStatusRequest": {
        "type": "object",
        "description": "Which asynchronous job to poll.",
        "required": [
          "job_id"
        ],
        "properties": {
          "job_id": {
            "type": "string",
            "description": "The job the submission answered.",
            "example": "4028808ca0b31c7d01a0b31d44e70000"
          }
        }
      },
      "WidgetInput": {
        "type": "object",
        "description": "One widget of a custom section: which integration it reads and what it shows.",
        "required": [
          "dsl"
        ],
        "properties": {
          "integration_id": {
            "type": "string",
            "description": "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": {
            "$ref": "#/components/schemas/WidgetDsl",
            "description": "What the widget shows. 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`."
          }
        },
        "additionalProperties": false
      },
      "CatalogSectionInput": {
        "type": "object",
        "description": "A section copied from a published catalog entry. The version is not sent: the version published today is always the one taken.",
        "required": [
          "type",
          "slug",
          "position",
          "add_summary",
          "show_previous_period"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "catalog"
            ]
          },
          "slug": {
            "type": "string",
            "description": "The catalog entry the section is copied from."
          },
          "position": {
            "type": "integer",
            "description": "0-based position of the section inside that entry."
          },
          "add_summary": {
            "$ref": "#/components/schemas/SectionSummaryInput"
          },
          "show_previous_period": {
            "description": "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.",
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "additionalProperties": false
      },
      "CustomSectionInput": {
        "type": "object",
        "description": "A section written from scratch, with the widgets the request defines.",
        "required": [
          "type",
          "name",
          "widgets",
          "add_summary",
          "show_previous_period"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "custom"
            ]
          },
          "name": {
            "type": "string",
            "description": "Title of the section, between 1 and 80 characters.",
            "minLength": 1,
            "maxLength": 80
          },
          "widgets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WidgetInput"
            },
            "minItems": 1,
            "maxItems": 12,
            "description": "At most 12 widgets per section."
          },
          "add_summary": {
            "$ref": "#/components/schemas/SectionSummaryInput"
          },
          "show_previous_period": {
            "description": "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.",
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "additionalProperties": false
      },
      "SectionSummaryInput": {
        "description": "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.",
        "anyOf": [
          {
            "type": "boolean"
          },
          {
            "type": "null"
          },
          {
            "type": "object",
            "required": [
              "position"
            ],
            "properties": {
              "position": {
                "type": "string",
                "enum": [
                  "first",
                  "last"
                ],
                "description": "`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."
              }
            },
            "additionalProperties": false
          }
        ]
      },
      "SectionInput": {
        "description": "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.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/CatalogSectionInput"
          },
          {
            "$ref": "#/components/schemas/CustomSectionInput"
          }
        ]
      },
      "DataSourceInput": {
        "type": "object",
        "description": "One report level binding. Every widget of the report reads its integration from here.",
        "required": [
          "integration_id",
          "connection_key",
          "account_id"
        ],
        "properties": {
          "integration_id": {
            "type": "string",
            "description": "Which integration this binding is for. Two entries may not name the same one."
          },
          "connection_key": {
            "type": "string",
            "description": "One of the connection keys of /v1/connections."
          },
          "account_id": {
            "type": "string",
            "description": "One of the account ids of that connection."
          },
          "entity": {
            "type": "string",
            "description": "REQUIRED for mailchimp and rejected for every other integration: which of its two levels this binding is for.",
            "enum": [
              "campaign",
              "list"
            ]
          }
        }
      },
      "CompositionDateRange": {
        "type": "object",
        "description": "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.",
        "required": [
          "preset"
        ],
        "properties": {
          "preset": {
            "type": "string",
            "description": "The period of the report.",
            "enum": [
              "today",
              "yesterday",
              "lastweek",
              "thisweek",
              "lastmonth",
              "thismonth",
              "lastxdays",
              "lastxweeks",
              "lastxmonths",
              "yeartolastmonth",
              "yeartolastweek",
              "custom"
            ]
          },
          "x": {
            "type": "integer",
            "description": "How many days, weeks or months. Required by the lastx* presets."
          },
          "start": {
            "type": "string",
            "description": "First day of a custom period, as yyyy-MM-dd."
          },
          "end": {
            "type": "string",
            "description": "Last day of a custom period, as yyyy-MM-dd."
          }
        }
      },
      "AddSummary": {
        "description": "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.",
        "oneOf": [
          {
            "type": "boolean"
          },
          {
            "type": "object",
            "properties": {
              "placement": {
                "type": "string",
                "enum": [
                  "own_section",
                  "first_section"
                ]
              }
            },
            "additionalProperties": false
          }
        ]
      },
      "ReportsListRequest": {
        "type": "object",
        "description": "No parameters.",
        "properties": {},
        "additionalProperties": true
      },
      "ReportsListData": {
        "type": "object",
        "description": "Every report of the account, ordered by title.",
        "required": [
          "reports",
          "count"
        ],
        "properties": {
          "reports": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReportHeader"
            }
          },
          "count": {
            "type": "integer",
            "description": "How many rows came back. The list is never paginated."
          }
        }
      },
      "ReportsListResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/ReportsListData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "ReportsTrashRequest": {
        "type": "object",
        "description": "No parameters.",
        "properties": {},
        "additionalProperties": true
      },
      "TrashedReport": {
        "description": "A report in the trash: the header every report has, with the type it had before it was deleted, plus when it was deleted and until when it can be restored.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ReportHeader"
          },
          {
            "type": "object",
            "required": [
              "trashed_at",
              "restorable_until"
            ],
            "properties": {
              "trashed_at": {
                "description": "When the report was put in the trash, in UTC.",
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "restorable_until": {
                "description": "Until when /v1/reports/restore brings this report back, in UTC. After that it is deleted for good.",
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        ]
      },
      "ReportsTrashData": {
        "type": "object",
        "description": "The reports of the account that are in the trash, most recently deleted first.",
        "required": [
          "reports",
          "count"
        ],
        "properties": {
          "reports": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TrashedReport"
            }
          },
          "count": {
            "type": "integer",
            "description": "How many rows came back. The list is never paginated."
          }
        }
      },
      "ReportsTrashResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/ReportsTrashData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "TemplatesListRequest": {
        "type": "object",
        "description": "No parameters.",
        "properties": {},
        "additionalProperties": true
      },
      "TemplatesListData": {
        "type": "object",
        "description": "The account's own templates, with the same header a report has.",
        "required": [
          "templates",
          "count"
        ],
        "properties": {
          "templates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReportHeader"
            }
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "TemplatesListResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/TemplatesListData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "ReportsGetRequest": {
        "type": "object",
        "description": "Which report to read.",
        "required": [
          "report_key"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report to read, from /v1/reports/list or /v1/templates/list."
          }
        }
      },
      "ReportsGetData": {
        "type": "object",
        "description": "The header of the report plus its sections and widgets.",
        "required": [
          "report_key",
          "name",
          "internal_name",
          "type",
          "based_on_template_key",
          "catalog",
          "created_via",
          "version",
          "created_at",
          "report_level_data_sources",
          "data_sources",
          "share_link_enabled",
          "share_link_id",
          "share_url",
          "app_url",
          "share_link_password_set",
          "share_link_allow_date_range",
          "settings",
          "sections",
          "widget_count",
          "editable_count",
          "truncated",
          "unreadable_section_keys"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The key that addresses this report in every other endpoint."
          },
          "name": {
            "description": "Title of the report: the name the cover paints, and the one every write takes as `name`. At most 60 characters.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The OTHER name of the report: the one the list of the application shows. A report has two and both are writable. At most 100 characters."
          },
          "type": {
            "type": "string",
            "description": "`report` is a normal report, `template` one of the account's own templates, `template_based` a report whose sections are its template's.",
            "enum": [
              "report",
              "template",
              "template_based"
            ]
          },
          "based_on_template_key": {
            "description": "The template a template_based report follows. Null for the other two types.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "catalog": {
            "description": "The catalog entry this report was materialized from, or null when it was not.",
            "anyOf": [
              {
                "type": "object",
                "required": [
                  "slug",
                  "version"
                ],
                "properties": {
                  "slug": {
                    "type": "string"
                  },
                  "version": {
                    "anyOf": [
                      {
                        "type": "integer"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "created_via": {
            "type": "string",
            "description": "Whether the report was created through this API or in the app.",
            "enum": [
              "api",
              "ui"
            ]
          },
          "version": {
            "type": "integer",
            "description": "Version of the sections structure. It is what if_version compares against, and it moves only when the list of sections changes."
          },
          "created_at": {
            "description": "When the report was created, in UTC.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_level_data_sources": {
            "type": "boolean",
            "description": "True when every widget reads from the bindings of data_sources; false when each widget carries its own connection, which is a report the API cannot extend."
          },
          "data_sources": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReportDataSource"
            },
            "description": "The report level bindings, ordered by integration. A binding without an account is not published."
          },
          "share_link_enabled": {
            "type": "boolean",
            "description": "Whether the public share link of the report is on."
          },
          "share_link_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The id in the public /share URL, or null when the report has never been shared."
          },
          "share_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The public URL of the report, built on the account's own portal domain when it has one. Null when the report has no link."
          },
          "app_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The link that opens this report in the Reporting Ninja application. Opening it requires a signed-in user of the account, so it is not the public link: `share_url` is the one anybody holding it reads. Null for a report in the trash, which the application does not open."
          },
          "share_link_password_set": {
            "type": "boolean",
            "description": "Whether the link asks for a password. The password itself is never published."
          },
          "share_link_allow_date_range": {
            "type": "boolean",
            "description": "Whether whoever opens the link may change the period."
          },
          "settings": {
            "$ref": "#/components/schemas/ReportSettings",
            "description": "The settings window of the report. Present in the answer of /v1/reports/get and of /v1/reports/update, which are about one report; the listings leave it out."
          },
          "sections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReportSection"
            }
          },
          "widget_count": {
            "type": "integer",
            "description": "Widgets in the answer."
          },
          "editable_count": {
            "type": "integer",
            "description": "How many of them carry a DSL definition."
          },
          "truncated": {
            "type": "boolean",
            "description": "True when the report holds more than 500 widgets and the answer was cut on a section boundary."
          },
          "unreadable_section_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Sections that could not be read. A broken section never takes the whole report down."
          }
        }
      },
      "ReportsGetResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/ReportsGetData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "CatalogListRequest": {
        "type": "object",
        "description": "Which catalog to read and how much of it.",
        "properties": {
          "language": {
            "type": "string",
            "description": "Language of the names and descriptions. Default en.",
            "enum": [
              "en",
              "es"
            ]
          },
          "include_sections": {
            "type": "boolean",
            "description": "Add the sections and the parameters of every entry. Off by default because it costs a full read of each published version; turn it on when you are about to compose with a catalog section, because that is where its position comes from."
          }
        }
      },
      "CatalogListData": {
        "type": "object",
        "description": "The published catalog. It is global: every account sees the same entries.",
        "required": [
          "templates",
          "count"
        ],
        "properties": {
          "templates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CatalogTemplate"
            }
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "CatalogListResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/CatalogListData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "UsersListRequest": {
        "type": "object",
        "description": "No parameters.",
        "properties": {},
        "additionalProperties": true
      },
      "UsersListData": {
        "type": "object",
        "description": "The client users of the account, ordered by email address.",
        "required": [
          "users",
          "count"
        ],
        "properties": {
          "users": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClientUser"
            }
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "UsersListResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/UsersListData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "SchedulesListRequest": {
        "type": "object",
        "description": "Which schedules to list.",
        "properties": {
          "report_key": {
            "type": "string",
            "description": "Only the schedules of this report. When absent, every schedule of the account."
          }
        }
      },
      "SchedulesListData": {
        "type": "object",
        "description": "The scheduled deliveries, ordered by report. A one-off delivery that is waiting to go out is not one of them.",
        "required": [
          "schedules",
          "count"
        ],
        "properties": {
          "schedules": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Schedule"
            }
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "SchedulesListResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/SchedulesListData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "PortalViewsListRequest": {
        "type": "object",
        "description": "No parameters.",
        "properties": {},
        "additionalProperties": true
      },
      "PortalViewsListData": {
        "type": "object",
        "description": "The views of the client portal, ordered by name.",
        "required": [
          "portal_views",
          "count"
        ],
        "properties": {
          "portal_views": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PortalView"
            }
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "PortalViewsListResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/PortalViewsListData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "CapabilitiesRequest": {
        "type": "object",
        "description": "No parameters.",
        "properties": {},
        "additionalProperties": true
      },
      "CapabilitiesData": {
        "type": "object",
        "description": "Twenty-six blocks describing what this version of the API can do, plus endpoints_note. Every one of them is answered on every call; they are documented here by what they carry rather than field by field, because the ceilings themselves are what the endpoint exists to publish and reading them from the answer is the point. The example shows every block, with the longer ones cut down to their first few fields.",
        "required": [
          "contract",
          "widgets",
          "comparators",
          "integrations",
          "sources_without_dsl",
          "query_not_available",
          "date_presets",
          "composition",
          "summary",
          "ai_briefs",
          "schedules",
          "portal_views",
          "users",
          "versioning",
          "reports_get",
          "filters",
          "calculated_metrics",
          "brand_templates",
          "language_files",
          "email_templates",
          "snapshots",
          "reports",
          "editing",
          "guarantees",
          "endpoints",
          "endpoints_note",
          "account"
        ],
        "properties": {
          "contract": {
            "type": "object",
            "description": "The release this surface is at, which DSL and which composition vocabulary it speaks, whether this version adds anything to them, which keys it added and which limits have come down since the GA -- each with the day and the version that carried it -- how many problems an answer lists, and that unknown fields and characters outside the Basic Multilingual Plane are refused.",
            "additionalProperties": true
          },
          "widgets": {
            "type": "object",
            "description": "The six shapes a widget can take, the rule each one imposes on its metrics and dimensions, how many of each it accepts, and which chart the two shapes that draw one can be drawn as.",
            "additionalProperties": true
          },
          "comparators": {
            "type": "object",
            "description": "The comparator half of the widget DSL: the four shapes a comparator is drawn as, the three classes the DSL describes, the time axes, how many things each class compares, and the two kinds of comparator this API does not describe at all.",
            "additionalProperties": true
          },
          "integrations": {
            "type": "array",
            "description": "Every integration the DSL can build a widget for, with its data views and whether each one is covered. A data_view published as an empty string is the word of the DSL for a source with a single view: the widget sends it as an empty string too.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "sources_without_dsl": {
            "type": "array",
            "description": "The sources whose widgets cannot be expressed as a DSL, each named the way every other endpoint names an integration: their widgets can be read as not editable and cannot be created.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "query_not_available": {
            "type": "object",
            "description": "The integrations whose structure is published and whose rows cannot be read through /v1/query, with the reason.",
            "additionalProperties": true
          },
          "date_presets": {
            "type": "object",
            "description": "The periods a report can be built for, and the query presets that have no equivalent among them.",
            "additionalProperties": true
          },
          "composition": {
            "type": "object",
            "description": "The ceilings and rules of a composition: sections per report, widgets per section and per composition, the data sources, the summary, the languages, the template parameters, the placement of a widget and what a section delete does.",
            "additionalProperties": true
          },
          "summary": {
            "type": "object",
            "description": "What the AI report summary costs and where it can be placed.",
            "additionalProperties": true
          },
          "ai_briefs": {
            "type": "object",
            "description": "The three levels of client brief an AI summary reads -- account, report and widget -- which two of them this API writes, and what a brief is not part of.",
            "additionalProperties": true
          },
          "schedules": {
            "type": "object",
            "description": "The frequencies, the formats, the hours, the recipient ceilings, the length of every email field and the two kinds of row this version refuses to update.",
            "additionalProperties": true
          },
          "portal_views": {
            "type": "object",
            "description": "The ceilings of a view, which fields an update replaces wholesale, and what asking for a notification implies.",
            "additionalProperties": true
          },
          "users": {
            "type": "object",
            "description": "What creating a client user requires and what it sends.",
            "additionalProperties": true
          },
          "versioning": {
            "type": "object",
            "description": "What if_version covers, which endpoints accept it, and that sending it anywhere else is refused rather than ignored.",
            "additionalProperties": true
          },
          "reports_get": {
            "type": "object",
            "description": "The widget ceiling of /v1/reports/get and what happens above it.",
            "additionalProperties": true
          },
          "reports": {
            "type": "object",
            "description": "The four writes of a report that already exists: which fields /v1/reports/update takes, what it never writes, what is not available in this version yet, and the rules of the bulk rebinding, the copy and the duplication for another client.",
            "additionalProperties": true
          },
          "editing": {
            "type": "array",
            "description": "What this surface deliberately cannot express, statement by statement: each entry carries a `key` to switch on, a `statement` a person reads, and, where a call really is refused for it, a `refused_with` with the `error_code` and the named `reason` that come back. An entry with no `refused_with` is a difference from the editor rather than a wall.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "guarantees": {
            "type": "object",
            "description": "How long a confirmation token lives, how long an answer can be replayed, what a dry run costs, which call undoes which, the daily email budget, the ceilings of verify_data and, in `trash`, how long a deleted report stays restorable and what a restore does and does not bring back.",
            "additionalProperties": true
          },
          "endpoints": {
            "type": "array",
            "description": "One entry per write endpoint: whether it always confirms, confirms conditionally or never does, which fields trigger a conditional confirmation, whether it accepts if_version, and the endpoint that undoes it -- plus an undo_note where that endpoint does not put everything back. The undo published for /reports/delete holds only while permanent is false.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "endpoints_note": {
            "type": "string",
            "description": "Says that endpoints[] describes the calls that change something. The reads are listed by the block that describes them."
          },
          "account": {
            "type": "object",
            "description": "The only per-account block: the plan's room for reports and templates, how much of it is used, and whether write access is enabled for the key that is calling.",
            "additionalProperties": true
          },
          "filters": {
            "description": "What a read of a saved filter publishes, the shape of its conditions and why one can come back without them."
          },
          "calculated_metrics": {
            "description": "The three kinds of calculated metric and the closed vocabulary of each block."
          },
          "brand_templates": {
            "description": "The closed catalogues of a brand template and what a read says of its two images."
          },
          "language_files": {
            "description": "The sixteen language files of the platform and what a listing does not publish."
          },
          "email_templates": {
            "description": "The formats of an email template and what a listing does not publish."
          },
          "snapshots": {
            "description": "The snapshots of a report: that the file is taken later and not by the call, that it spends AI credits, that it is not queued twice (ALREADY_QUEUED), the retention of 13 per report, and the signed link of the download, which is REST only and has no tool."
          }
        },
        "additionalProperties": true
      },
      "CapabilitiesResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/CapabilitiesData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "ReportsCreateRequest": {
        "type": "object",
        "description": "A composition: what to call the report, which connections it reads from and which sections it is made of.",
        "required": [
          "name",
          "sections"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "The title of the report, 1 to 60 characters: the name the cover paints. It is what /v1/reports/get answers as `name`.",
            "maxLength": 60,
            "minLength": 1
          },
          "internal_name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "description": "The name the list of the application shows, 1 to 100 characters. Left out, it is the title."
          },
          "type": {
            "type": "string",
            "description": "What to create. Default report.",
            "enum": [
              "report",
              "template"
            ]
          },
          "language": {
            "type": "string",
            "description": "Language of the report's own literals. Defaults to the account owner's.",
            "enum": [
              "en",
              "es"
            ]
          },
          "date_range": {
            "$ref": "#/components/schemas/CompositionDateRange"
          },
          "add_summary": {
            "$ref": "#/components/schemas/AddSummary"
          },
          "data_sources": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DataSourceInput"
            },
            "description": "The report level bindings. One entry per integration at most; every integration a section reads from needs one."
          },
          "sections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SectionInput"
            },
            "minItems": 1,
            "maxItems": 12,
            "description": "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."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          }
        },
        "additionalProperties": false
      },
      "ReportsCreateData": {
        "type": "object",
        "description": "The report that was created, with the key of every section and widget it now holds.",
        "required": [
          "report_key",
          "app_url",
          "type",
          "name",
          "version",
          "created_via",
          "sections",
          "created_keys",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "app_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "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": {
            "type": "string",
            "enum": [
              "report",
              "template",
              "template_based"
            ]
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "version": {
            "type": "number",
            "description": "The version its sections structure starts at."
          },
          "created_via": {
            "type": "string",
            "enum": [
              "api"
            ]
          },
          "sections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WrittenSection"
            }
          },
          "created_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Every key this call created, flat, in the order they were written."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "ReportsCreateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "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.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/ReportsCreateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "ReportsCreateFromTemplateRequest": {
        "type": "object",
        "description": "Which catalog entry to materialize and how.",
        "required": [
          "catalog_slug"
        ],
        "properties": {
          "catalog_slug": {
            "type": "string",
            "description": "The published entry to materialize, from /v1/catalog/list."
          },
          "version": {
            "type": "integer",
            "description": "A published version of that entry. Defaults to the one published today."
          },
          "name": {
            "type": "string",
            "description": "Title of the report. Defaults to the name of the entry.",
            "minLength": 1,
            "maxLength": 60
          },
          "internal_name": {
            "type": "string",
            "description": "Name for the account's own staff. Defaults to the entry's.",
            "minLength": 1,
            "maxLength": 100
          },
          "language": {
            "type": "string",
            "description": "Language of the report's own literals. Defaults to the account owner's.",
            "enum": [
              "en",
              "es"
            ]
          },
          "language_file_key": {
            "type": "string",
            "description": "One of the system language files, or a language file of the account."
          },
          "section_positions": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "Which sections of the entry to materialize, as 0-based positions, each one inside the range and never repeated. When absent, all of them."
          },
          "as": {
            "type": "string",
            "description": "Materialize the entry as a report or as one of the account's own templates. Default report.",
            "enum": [
              "report",
              "template"
            ]
          },
          "parameter_values": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "maxLength": 80
            },
            "description": "The values a parameterized entry asks for, keyed by parameter id, each at most 80 characters. Only for a parameterized entry, and only when `as` is report."
          },
          "data_sources": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DataSourceInput"
            },
            "description": "The report level bindings of the new report."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          }
        },
        "additionalProperties": false
      },
      "ReportsCreateFromTemplateData": {
        "type": "object",
        "description": "The report that was materialized from the catalog entry.",
        "required": [
          "report_key",
          "app_url",
          "type",
          "name",
          "internal_name",
          "version",
          "created_via",
          "sections",
          "created_keys",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "app_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "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": {
            "type": "string",
            "enum": [
              "report",
              "template",
              "template_based"
            ]
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "version": {
            "type": "number"
          },
          "created_via": {
            "type": "string",
            "enum": [
              "api"
            ]
          },
          "sections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WrittenSection"
            },
            "description": "The sections that were written, with their widgets, so the next call can address them without reading the report back."
          },
          "created_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Every key this call created, flat, in the order they were written."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "ReportsCreateFromTemplateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "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.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/ReportsCreateFromTemplateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "ReportsAppendRequest": {
        "type": "object",
        "description": "Which report to extend and with what. add_summary is not read here, and data_sources is refused.",
        "required": [
          "report_key",
          "sections"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report the sections are added to."
          },
          "sections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SectionInput"
            },
            "minItems": 1,
            "maxItems": 12,
            "description": "The sections to add, in the same vocabulary /v1/reports/create takes."
          },
          "date_range": {
            "$ref": "#/components/schemas/CompositionDateRange"
          },
          "language": {
            "type": "string",
            "description": "Language of the literals of the new sections. Defaults to the account owner's.",
            "enum": [
              "en",
              "es"
            ]
          },
          "data_sources": {
            "type": "array",
            "description": "Read by name and always refused with INVALID_VALUE: an append does not change what a report is bound to. It is named here so the refusal says that, instead of the generic UNKNOWN_PARAM a field this endpoint does not know would get.",
            "items": {
              "$ref": "#/components/schemas/DataSourceInput"
            }
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          },
          "if_version": {
            "type": "number",
            "description": "The version of the report's sections structure this call expects. A mismatch is HTTP 409 VERSION_MISMATCH and nothing is written."
          }
        },
        "additionalProperties": false
      },
      "ReportsAppendData": {
        "type": "object",
        "description": "What the append added and the version the report is now at.",
        "required": [
          "report_key",
          "version",
          "sections",
          "created_keys",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "version": {
            "type": "number",
            "description": "The version the sections structure moved to."
          },
          "sections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WrittenSection"
            },
            "description": "Only the sections this call added."
          },
          "created_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The sections and widgets that were written. The report is not one of them: it already existed."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "ReportsAppendResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "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.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/ReportsAppendData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "ReportsDeleteRequest": {
        "type": "object",
        "description": "Which report to delete.",
        "required": [
          "report_key"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report to delete."
          },
          "permanent": {
            "type": "boolean",
            "description": "Delete the report for good instead of putting it in the trash. Default false. A report that is already in the trash is only accepted with this set to true; without it the answer is 409 REPORT_IN_TRASH."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          },
          "confirmation_token": {
            "type": "string",
            "description": "The token the previous call answered. Sending the same body with it performs the write."
          }
        },
        "additionalProperties": false
      },
      "ReportsDeleteData": {
        "type": "object",
        "description": "The report is gone. Without permanent it went to the trash, keeping its sections, its widgets and its bindings, and /v1/reports/restore brings it back until restorable_until; with permanent it was removed with everything inside it. Its schedules are deleted either way.",
        "required": [
          "report_key",
          "deleted",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "deleted": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "permanent": {
            "type": "boolean",
            "description": "True when the report was removed for good, false when it went to the trash."
          },
          "trashed_at": {
            "description": "When the report was put in the trash, in UTC. Only present when permanent is false.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "restorable_until": {
            "description": "Until when /v1/reports/restore brings this report back, in UTC. After that it is deleted for good. Only present when permanent is false.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "restore": {
            "type": "string",
            "description": "The endpoint that undoes this deletion. Only present when permanent is false."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "ReportsDeleteResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/ReportsDeleteData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "ReportsRestoreRequest": {
        "type": "object",
        "description": "Which report to take out of the trash.",
        "required": [
          "report_key"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report to restore, as /v1/reports/trash lists it."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          }
        },
        "additionalProperties": false
      },
      "ReportsRestoreData": {
        "type": "object",
        "description": "The report is back in the account, with the type it had before it was deleted. Its schedules are not: create them again.",
        "required": [
          "report_key",
          "app_url",
          "restored",
          "type",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "app_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The link that opens the report in the Reporting Ninja application again, now that it is out of the trash. Opening it requires a signed-in user of the account; it is not the public link, which is `share_url`."
          },
          "restored": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "type": {
            "type": "string",
            "description": "The type the report has again: the one it had before it was deleted.",
            "enum": [
              "report",
              "template",
              "template_based"
            ]
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "ReportsRestoreResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, carrying the preview of this operation alongside its own fields, so an answer can match more than one of these shapes. This endpoint never asks for a confirmation.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/ReportsRestoreData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "WidgetsCreateRequest": {
        "type": "object",
        "description": "Which section to add a widget to, and what the widget shows: a definition, or an AI summary.",
        "required": [
          "report_key",
          "section_key"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report the section belongs to."
          },
          "section_key": {
            "type": "string",
            "description": "The section the widget is added to."
          },
          "integration_id": {
            "type": "string",
            "description": "Which integration the widget reads from. The report must already have a data source for it. 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": {
            "$ref": "#/components/schemas/WidgetDsl",
            "description": "What the widget shows. 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 body carries a dsl or an ai_summary, exactly one of the two."
          },
          "ai_summary": {
            "$ref": "#/components/schemas/WidgetAiSummary",
            "description": "An AI summary instead of a definition: the platform writes it for each period, so it has no dsl and no text to edit. It carries what the summary reads and, optionally, the settings it is generated with and its brief; a setting the block does not name is left for the platform to resolve. Mutually exclusive with dsl, and one of the two is required. 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 answer says how many. Over a ceiling of /v1/capabilities the call is HTTP 409 AI_SUMMARY_NOT_ALLOWED with the rule that stopped it in details.reason."
          },
          "connection_key": {
            "type": "string",
            "description": "The connection this widget reads through, and ONLY for a report whose report_level_data_sources is false, which binds a connection per widget: those reports refuse a widget that brings none, and a report that binds once for the whole report refuses one that brings it. Take it from /v1/connections. A widget that reads no data -- a text block, an AI summary -- takes none. An ai_summary never takes one: it reads the widgets of its report and no data source of its own."
          },
          "account_id": {
            "type": "string",
            "description": "The account within that connection. It travels with connection_key and is checked against the accounts /v1/connections publishes for it."
          },
          "placement": {
            "type": "object",
            "description": "Where to put the widget. When absent, it is placed automatically. Only row and column are read here; anything else inside this object is ignored rather than refused.",
            "required": [
              "row"
            ],
            "properties": {
              "row": {
                "type": "integer",
                "description": "1-based row. It must be an existing row or the first free one, and it must respect how that row is already filled: a refusal names the first free row in details.first_free_row."
              },
              "column": {
                "type": "integer",
                "description": "1-based column. A column that is taken is moved to the first free one and the answer says so with placement_adjusted."
              }
            },
            "additionalProperties": true
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          },
          "if_version": {
            "type": "number",
            "description": "The version of the report's sections structure this call expects. A mismatch is HTTP 409 VERSION_MISMATCH and nothing is written."
          },
          "verify_data": {
            "type": "boolean",
            "description": "Run the widget against the provider before writing it, waiting up to 45 seconds, and publish the verdict as data.verification. A failure verdict refuses the write with HTTP 422. Default false."
          }
        },
        "additionalProperties": false
      },
      "WidgetsCreateData": {
        "type": "object",
        "description": "The widget that was created, where it landed and, when verify_data was asked for, the verdict of running it.",
        "required": [
          "report_key",
          "section_key",
          "name",
          "integration_id",
          "row",
          "column",
          "placement_adjusted",
          "widget_key",
          "created_keys",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "section_key": {
            "type": "string"
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "integration_id": {
            "type": "string"
          },
          "row": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "column": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "placement_adjusted": {
            "type": "boolean",
            "description": "True when the column asked for was taken and the widget went to the first free one."
          },
          "widget_key": {
            "type": "string"
          },
          "created_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Every key this call created, flat, in the order they were written."
          },
          "verification": {
            "$ref": "#/components/schemas/Verification"
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          },
          "ai_summary": {
            "$ref": "#/components/schemas/WidgetAiSummary",
            "description": "Present only when the write was about an AI summary: the scope it has, the selection it ended up with -- the one the body named, or the one the platform picked -- and what one generation of it costs. It confirms the SELECTION and not the whole block: the settings and the brief the call wrote are read back in widget.ai_summary."
          }
        }
      },
      "WidgetsCreateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "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.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/WidgetsCreateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "WidgetsDeleteRequest": {
        "type": "object",
        "description": "Which widget to delete.",
        "required": [
          "report_key",
          "section_key",
          "widget_key"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "section_key": {
            "type": "string"
          },
          "widget_key": {
            "type": "string",
            "description": "The widget to delete. It must belong to that section of that report."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          },
          "if_widget_hash": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "The widget_hash /v1/widgets/get published for this widget. The deletion is refused with HTTP 409 WIDGET_CHANGED when the definition changed in between, so a widget somebody has just rewritten is not deleted under the answer you read. The confirmation token does not cover this: it is bound to the body, and the body of a deletion says nothing about what the widget contains."
          },
          "confirmation_token": {
            "type": "string",
            "description": "The token the previous call answered. Sending the same body with it performs the write."
          }
        },
        "additionalProperties": false
      },
      "WidgetsDeleteData": {
        "type": "object",
        "description": "The widget is gone.",
        "required": [
          "widget_key",
          "section_key",
          "report_key",
          "deleted",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "widget_key": {
            "type": "string"
          },
          "section_key": {
            "type": "string"
          },
          "report_key": {
            "type": "string"
          },
          "deleted": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "WidgetsDeleteResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/WidgetsDeleteData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "SectionsDeleteRequest": {
        "type": "object",
        "description": "Which section to delete.",
        "required": [
          "report_key",
          "section_key"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "section_key": {
            "type": "string",
            "description": "The section to delete, with the widgets it holds."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          },
          "if_version": {
            "type": "number",
            "description": "The version of the report's sections structure this call expects. A mismatch is HTTP 409 VERSION_MISMATCH and nothing is written."
          },
          "confirmation_token": {
            "type": "string",
            "description": "The token the previous call answered. Sending the same body with it performs the write."
          }
        },
        "additionalProperties": false
      },
      "SectionsDeleteData": {
        "type": "object",
        "description": "The section is gone with its widgets, and the report moved to a new version.",
        "required": [
          "section_key",
          "report_key",
          "version",
          "deleted",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "section_key": {
            "type": "string"
          },
          "report_key": {
            "type": "string"
          },
          "version": {
            "type": "number",
            "description": "The version the report's sections structure moved to."
          },
          "deleted": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "SectionsDeleteResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/SectionsDeleteData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "SchedulesCreateRequest": {
        "type": "object",
        "description": "The delivery to configure. A report has at most one standing schedule; when it already has one and it is delivering, use /v1/schedules/update. What is checked is the RESULT: `time` must end up set unless the frequency is now, and at least one recipient must end up in recipients_to. When this call is configuring a schedule that had been stopped, a field the body leaves out keeps the value it already had, so the two can come from there instead of from this body.",
        "required": [
          "report_key",
          "frequency"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report to deliver. It cannot be a template."
          },
          "frequency": {
            "type": "string",
            "description": "How often to deliver it. `now` sends it once, as soon as the next delivery pass runs, and leaves no standing order behind. `none` is not accepted here: use /v1/schedules/delete to stop a delivery.",
            "enum": [
              "daily",
              "weekly",
              "monthly",
              "now"
            ]
          },
          "frequency_details": {
            "type": "string",
            "description": "REQUIRED with weekly and with monthly, rejected with any other frequency. With weekly it is the weekday in upper case: MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY or SUNDAY. With monthly it is the day of the month written as a string, from \"1\" to \"31\"."
          },
          "time": {
            "type": "string",
            "description": "Hour of the delivery in the time zone of the account, as HH:00. REQUIRED except with frequency now, which rejects it."
          },
          "recipients_to": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 255
            },
            "maxItems": 20,
            "description": "At least one address, at most 20, each at most 255 characters."
          },
          "recipients_cc": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 255
            },
            "maxItems": 20
          },
          "recipients_bcc": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 255
            },
            "maxItems": 20
          },
          "report_format": {
            "type": "string",
            "description": "How the report travels. Default pdf. `link` puts a link in the body instead of an attachment and needs the report link token in it.",
            "enum": [
              "pdf",
              "xls",
              "link"
            ]
          },
          "email_template_key": {
            "description": "An email template of the account, which then carries the whole message. Explicit null means no template. When a template is named, the six email fields below are rejected by name.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "format": {
            "type": "string",
            "description": "Body of the email. REQUIRED when no email_template_key is named.",
            "enum": [
              "html",
              "plain_text"
            ]
          },
          "sender": {
            "type": "string",
            "description": "Display name of the sender. REQUIRED when no email_template_key is named.",
            "minLength": 5,
            "maxLength": 70
          },
          "sender_email": {
            "type": "string",
            "description": "REQUIRED when no email_template_key is named. It must be the address of a user of the account, or of a domain the account already sends from.",
            "maxLength": 255
          },
          "reply_to_email": {
            "type": "string",
            "description": "REQUIRED when no email_template_key is named, under the same restriction as sender_email.",
            "maxLength": 255
          },
          "subject": {
            "type": "string",
            "description": "REQUIRED when no email_template_key is named.",
            "minLength": 5,
            "maxLength": 150
          },
          "body": {
            "type": "string",
            "description": "REQUIRED when no email_template_key is named.",
            "minLength": 5,
            "maxLength": 75000
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          },
          "confirmation_token": {
            "type": "string",
            "description": "The token the previous call answered. Sending the same body with it performs the write."
          }
        },
        "additionalProperties": false
      },
      "SchedulesCreateData": {
        "type": "object",
        "description": "The schedule as it now stands, with the title of its report and, when there was a stopped row to reconfigure, what it looked like before. A one-off delivery answers a different shape: schedule_key, report_key, frequency now, transient: true and will_send_now_to.",
        "required": [
          "schedule_key",
          "report_key",
          "frequency",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "schedule_key": {
            "description": "The key that addresses this schedule.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_key": {
            "description": "The report that is delivered.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "frequency": {
            "description": "How often the report is sent. `none` is a schedule that is kept and does not deliver; `now` appears only in the answer of a one-off delivery.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "daily",
                  "weekly",
                  "monthly",
                  "none",
                  "now"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "frequency_details": {
            "description": "The weekday in upper case for `weekly`, and the day of the month as a string from \"1\" to \"31\" for `monthly`.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "time": {
            "description": "Hour of the delivery in the time zone of the account, as HH:00.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "recipients_to": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "recipients_cc": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "recipients_bcc": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "report_format": {
            "description": "How the report travels: attached as a PDF, attached as a spreadsheet, or as a link in the body.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "pdf",
                  "xls",
                  "link"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "email_template_key": {
            "description": "The email template of the account this delivery uses, or null when it uses none and when it uses the system one.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "uses_system_template": {
            "type": "boolean",
            "description": "True when the delivery is built from the system template in the language of the user, which this version cannot express and cannot update."
          },
          "format": {
            "description": "Body of the email.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "html",
                  "plain_text"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "subject": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "sender": {
            "description": "Display name of the sender.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "sender_email": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "reply_to_email": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "origin": {
            "description": "Where the schedule was created.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "app",
                  "portal",
                  "api"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "user_key": {
            "description": "The user the delivery is attributed to.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "conditional_execution_enabled": {
            "type": "boolean",
            "description": "True when the delivery only goes out if a condition holds. This version cannot express the condition, and refuses to change a schedule that has one."
          },
          "queued": {
            "type": "boolean",
            "description": "True while this delivery is being sent. A schedule cannot be changed or stopped while it is true."
          },
          "report_name": {
            "description": "Title of the report that is delivered.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "before": {
            "$ref": "#/components/schemas/Schedule",
            "description": "The schedule as it was before this write. Present whenever there was one."
          },
          "transient": {
            "type": "boolean",
            "description": "Present only for frequency now: the delivery is one-off and leaves no standing order."
          },
          "will_send_now_to": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Present only for frequency now: the addresses the one-off delivery goes to."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "SchedulesCreateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/SchedulesCreateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "SchedulesUpdateRequest": {
        "type": "object",
        "description": "The schedule to change and the fields to change in it. The update is partial: a field the body does not carry keeps the value it already had. The result is then validated as a whole, exactly as a creation is.",
        "required": [
          "schedule_key"
        ],
        "properties": {
          "schedule_key": {
            "type": "string",
            "description": "The schedule to change, from /v1/schedules/list."
          },
          "report_key": {
            "type": "string",
            "description": "The report of the schedule. Optional, and when sent it must be the one the schedule already delivers; it cannot be moved to another report and cannot be null."
          },
          "frequency": {
            "type": "string",
            "description": "How often to deliver it. An update cannot send a report once (`now` belongs to /v1/schedules/create) and cannot stop a delivery (`none` belongs to /v1/schedules/delete).",
            "enum": [
              "daily",
              "weekly",
              "monthly"
            ]
          },
          "frequency_details": {
            "type": "string",
            "description": "REQUIRED with weekly and with monthly, rejected with any other frequency. With weekly it is the weekday in upper case: MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY or SUNDAY. With monthly it is the day of the month written as a string, from \"1\" to \"31\"."
          },
          "time": {
            "type": "string",
            "description": "Hour of the delivery in the time zone of the account, as HH:00."
          },
          "recipients_to": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 255
            },
            "maxItems": 20,
            "description": "At least one address, at most 20, each at most 255 characters."
          },
          "recipients_cc": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 255
            },
            "maxItems": 20
          },
          "recipients_bcc": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 255
            },
            "maxItems": 20
          },
          "report_format": {
            "type": "string",
            "description": "How the report travels. `link` puts a link in the body instead of an attachment and needs the report link token in it.",
            "enum": [
              "pdf",
              "xls",
              "link"
            ]
          },
          "email_template_key": {
            "description": "An email template of the account, which then carries the whole message. Explicit null means no template. When a template is named, the six email fields below are rejected by name.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "format": {
            "type": "string",
            "description": "Body of the email. REQUIRED when no email_template_key is named.",
            "enum": [
              "html",
              "plain_text"
            ]
          },
          "sender": {
            "type": "string",
            "description": "Display name of the sender. REQUIRED when no email_template_key is named.",
            "minLength": 5,
            "maxLength": 70
          },
          "sender_email": {
            "type": "string",
            "description": "REQUIRED when no email_template_key is named. It must be the address of a user of the account, or of a domain the account already sends from.",
            "maxLength": 255
          },
          "reply_to_email": {
            "type": "string",
            "description": "REQUIRED when no email_template_key is named, under the same restriction as sender_email.",
            "maxLength": 255
          },
          "subject": {
            "type": "string",
            "description": "REQUIRED when no email_template_key is named.",
            "minLength": 5,
            "maxLength": 150
          },
          "body": {
            "type": "string",
            "description": "REQUIRED when no email_template_key is named.",
            "minLength": 5,
            "maxLength": 75000
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          },
          "confirmation_token": {
            "type": "string",
            "description": "The token the previous call answered. Sending the same body with it performs the write."
          }
        },
        "additionalProperties": false
      },
      "SchedulesUpdateData": {
        "type": "object",
        "description": "The schedule after the change, with what it looked like before.",
        "required": [
          "schedule_key",
          "report_key",
          "frequency",
          "before",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "schedule_key": {
            "description": "The key that addresses this schedule.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_key": {
            "description": "The report that is delivered.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "frequency": {
            "description": "How often the report is sent. `none` is a schedule that is kept and does not deliver; `now` appears only in the answer of a one-off delivery.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "daily",
                  "weekly",
                  "monthly",
                  "none",
                  "now"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "frequency_details": {
            "description": "The weekday in upper case for `weekly`, and the day of the month as a string from \"1\" to \"31\" for `monthly`.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "time": {
            "description": "Hour of the delivery in the time zone of the account, as HH:00.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "recipients_to": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "recipients_cc": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "recipients_bcc": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "report_format": {
            "description": "How the report travels: attached as a PDF, attached as a spreadsheet, or as a link in the body.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "pdf",
                  "xls",
                  "link"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "email_template_key": {
            "description": "The email template of the account this delivery uses, or null when it uses none and when it uses the system one.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "uses_system_template": {
            "type": "boolean",
            "description": "True when the delivery is built from the system template in the language of the user, which this version cannot express and cannot update."
          },
          "format": {
            "description": "Body of the email.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "html",
                  "plain_text"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "subject": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "sender": {
            "description": "Display name of the sender.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "sender_email": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "reply_to_email": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "origin": {
            "description": "Where the schedule was created.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "app",
                  "portal",
                  "api"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "user_key": {
            "description": "The user the delivery is attributed to.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "conditional_execution_enabled": {
            "type": "boolean",
            "description": "True when the delivery only goes out if a condition holds. This version cannot express the condition, and refuses to change a schedule that has one."
          },
          "queued": {
            "type": "boolean",
            "description": "True while this delivery is being sent. A schedule cannot be changed or stopped while it is true."
          },
          "report_name": {
            "description": "Title of the report that is delivered.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "before": {
            "$ref": "#/components/schemas/Schedule",
            "description": "The schedule as it was before this write. Present whenever there was one."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "SchedulesUpdateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/SchedulesUpdateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "SchedulesDeleteRequest": {
        "type": "object",
        "description": "Which schedule to stop.",
        "required": [
          "schedule_key"
        ],
        "properties": {
          "schedule_key": {
            "type": "string",
            "description": "The schedule to stop."
          },
          "report_key": {
            "type": "string",
            "description": "Optional. When sent it must be the report this schedule already delivers; an explicit null is refused."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          },
          "confirmation_token": {
            "type": "string",
            "description": "The token the previous call answered. Sending the same body with it performs the write."
          }
        },
        "additionalProperties": false
      },
      "SchedulesDeleteData": {
        "type": "object",
        "description": "The delivery is stopped and the configuration is kept, or the queued snapshot is cancelled and its request is gone.",
        "required": [
          "schedule_key",
          "report_key",
          "frequency",
          "queued_snapshot_cancelled",
          "row_is_kept",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "schedule_key": {
            "type": "string"
          },
          "report_key": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "frequency": {
            "type": "string",
            "description": "Always none once the call has run: nothing of that key delivers anything any more.",
            "enum": [
              "none"
            ]
          },
          "queued_snapshot_cancelled": {
            "type": "boolean",
            "description": "True when the key addressed the request of a snapshot the platform had not taken yet, which this call cancelled. False for a scheduled delivery."
          },
          "row_is_kept": {
            "type": "boolean",
            "description": "True for a schedule: the configuration is kept and only the delivery stops, so /v1/schedules/update can turn it back on. False for a cancelled snapshot: the request of one file is not a configuration, so nothing is left behind."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "SchedulesDeleteResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/SchedulesDeleteData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "PortalViewsCreateRequest": {
        "type": "object",
        "description": "The view to create: which reports which client users see, and how.",
        "required": [
          "name",
          "report_keys",
          "user_keys"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Title of the view, 2 to 150 characters."
          },
          "internal_notes": {
            "type": "string",
            "description": "A note for the account's own staff, at most 250 characters. It is never shown in the portal."
          },
          "brand_template_key": {
            "description": "A brand template of the account. Explicit null means the default brand of the account.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "send_notification": {
            "type": "boolean",
            "description": "Email the users that the view is available. Default false. Asking for it makes the call ask for a confirmation."
          },
          "allow_to_change_date_range": {
            "type": "boolean",
            "description": "Let the users change the period of what they see. Default true."
          },
          "date_range_option": {
            "type": "string",
            "description": "Which periods the portal offers.",
            "enum": [
              "all",
              "last_monthly_periods"
            ]
          },
          "report_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "maxItems": 50,
            "description": "The reports of the view, in the order the portal menu will show them. Between 1 and 50, no repeats, and none of them a template. On an update the list REPLACES the stored one."
          },
          "user_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "maxItems": 25,
            "description": "The client users who see it. Between 1 and 25, no repeats, and each one a client user of the account: the owner and the administrators are refused. On an update the list REPLACES the stored one."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          },
          "confirmation_token": {
            "type": "string",
            "description": "The token the previous call answered. Sending the same body with it performs the write."
          }
        },
        "additionalProperties": false
      },
      "PortalViewsCreateData": {
        "type": "object",
        "description": "The view that was created, and who was emailed about it.",
        "required": [
          "portal_view_key",
          "name",
          "report_keys",
          "user_keys",
          "notified",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "portal_view_key": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "description": "A note for the account's own staff. It is never shown in the portal.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "brand_template_key": {
            "description": "The brand the portal wears for these users, or null for the default brand of the account.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "send_notification": {
            "type": "boolean",
            "description": "Whether the users are emailed that the view is available."
          },
          "allow_to_change_date_range": {
            "type": "boolean",
            "description": "Whether the users may change the period of what they see."
          },
          "date_range_option": {
            "description": "Which periods the portal offers: every period, or only whole past months. These are the two values this version names; a view configured with a period choice added later is published with the value it is stored under, outside this list.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "all",
                  "last_monthly_periods"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "report_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The reports of the view, in the order of the portal menu."
          },
          "user_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The client users who see it."
          },
          "notified": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The addresses that were actually emailed by this call. Empty when it notified nobody."
          },
          "before": {
            "$ref": "#/components/schemas/PortalView",
            "description": "The view as it was before this write. Present on an update."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "PortalViewsCreateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/PortalViewsCreateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "PortalViewsUpdateRequest": {
        "type": "object",
        "description": "The view to change and the fields to change in it. The update is partial, except that report_keys and user_keys REPLACE the stored lists rather than adding to them.",
        "required": [
          "portal_view_key"
        ],
        "properties": {
          "portal_view_key": {
            "type": "string",
            "description": "The view to change, from /v1/portal-views/list."
          },
          "name": {
            "type": "string",
            "description": "Title of the view, 2 to 150 characters."
          },
          "internal_notes": {
            "type": "string",
            "description": "A note for the account's own staff, at most 250 characters. It is never shown in the portal."
          },
          "brand_template_key": {
            "description": "A brand template of the account. Explicit null means the default brand of the account.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "send_notification": {
            "type": "boolean",
            "description": "Email the users that the view is available. Default false. Asking for it makes the call ask for a confirmation."
          },
          "allow_to_change_date_range": {
            "type": "boolean",
            "description": "Let the users change the period of what they see. Default true."
          },
          "date_range_option": {
            "type": "string",
            "description": "Which periods the portal offers.",
            "enum": [
              "all",
              "last_monthly_periods"
            ]
          },
          "report_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "maxItems": 50,
            "description": "The reports of the view, in the order the portal menu will show them. Between 1 and 50, no repeats, and none of them a template. On an update the list REPLACES the stored one."
          },
          "user_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "maxItems": 25,
            "description": "The client users who see it. Between 1 and 25, no repeats, and each one a client user of the account: the owner and the administrators are refused. On an update the list REPLACES the stored one."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          },
          "confirmation_token": {
            "type": "string",
            "description": "The token the previous call answered. Sending the same body with it performs the write."
          }
        },
        "additionalProperties": false
      },
      "PortalViewsUpdateData": {
        "type": "object",
        "description": "The view after the change, with what it looked like before and who was emailed.",
        "required": [
          "portal_view_key",
          "name",
          "report_keys",
          "user_keys",
          "before",
          "notified",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "portal_view_key": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "description": "A note for the account's own staff. It is never shown in the portal.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "brand_template_key": {
            "description": "The brand the portal wears for these users, or null for the default brand of the account.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "send_notification": {
            "type": "boolean",
            "description": "Whether the users are emailed that the view is available."
          },
          "allow_to_change_date_range": {
            "type": "boolean",
            "description": "Whether the users may change the period of what they see."
          },
          "date_range_option": {
            "description": "Which periods the portal offers: every period, or only whole past months. These are the two values this version names; a view configured with a period choice added later is published with the value it is stored under, outside this list.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "all",
                  "last_monthly_periods"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "report_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The reports of the view, in the order of the portal menu."
          },
          "user_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The client users who see it."
          },
          "notified": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The addresses that were actually emailed by this call. Empty when it notified nobody."
          },
          "before": {
            "$ref": "#/components/schemas/PortalView",
            "description": "The view as it was before this write. Present on an update."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "PortalViewsUpdateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/PortalViewsUpdateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "UsersCreateRequest": {
        "type": "object",
        "description": "The client user to create. The role is always client: this endpoint does not create staff.",
        "required": [
          "email",
          "first_name",
          "last_name"
        ],
        "properties": {
          "email": {
            "type": "string",
            "description": "The address the user signs in with. It is lowercased and must not belong to any user already.",
            "maxLength": 255
          },
          "first_name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 50
          },
          "last_name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 50
          },
          "language": {
            "type": "string",
            "description": "Language of the portal and of the emails for this user. Defaults to the account owner's.",
            "enum": [
              "en",
              "es"
            ]
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          },
          "confirmation_token": {
            "type": "string",
            "description": "The token the previous call answered. Sending the same body with it performs the write."
          }
        },
        "additionalProperties": false
      },
      "UsersCreateData": {
        "type": "object",
        "description": "The user that was created and where he signs in. The generated password is never published: it travels only in the welcome email.",
        "required": [
          "user_key",
          "email",
          "first_name",
          "last_name",
          "language",
          "role",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "user_key": {
            "type": "string"
          },
          "email": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "first_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "last_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "language": {
            "description": "Language of the portal and of the emails for this user.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "role": {
            "type": "string",
            "description": "Always `client`. The owner and the administrators of the account are not part of this surface.",
            "enum": [
              "client"
            ]
          },
          "portal_url": {
            "description": "Where the user signs in.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "UsersCreateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/UsersCreateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "WidgetBackup": {
        "type": "object",
        "description": "One backup of a widget: the key that puts it back and when it was taken.",
        "required": [
          "backup_key",
          "created_at"
        ],
        "properties": {
          "backup_key": {
            "type": "string",
            "description": "Send it to /v1/widgets/restore."
          },
          "created_at": {
            "description": "When the backup was taken, in UTC.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "WidgetsGetRequest": {
        "type": "object",
        "description": "Which widget to read.",
        "required": [
          "report_key",
          "section_key",
          "widget_key"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report the widget belongs to."
          },
          "section_key": {
            "type": "string",
            "description": "The section the widget is in, as /v1/reports/get names it."
          },
          "widget_key": {
            "type": "string",
            "description": "The widget to read, as /v1/reports/get names it. In a report built on a template that is the key of the copy the report keeps of its own, which is the one this endpoint answers for."
          }
        },
        "additionalProperties": false
      },
      "WidgetsGetData": {
        "type": "object",
        "description": "One widget, with everything an edit of it needs: its definition, the fingerprint that guards it, what a rewrite would cost and the backups it can be put back from.",
        "required": [
          "report_key",
          "section_key",
          "widget_key",
          "widget",
          "widget_hash",
          "lost_properties",
          "backup_keys"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "section_key": {
            "type": "string"
          },
          "widget_key": {
            "type": "string"
          },
          "widget": {
            "$ref": "#/components/schemas/ReportWidget",
            "description": "The same object /v1/reports/get publishes for this widget."
          },
          "widget_hash": {
            "description": "Fingerprint of the definition, the format, the custom header literals, the connection and the internal notes of the widget. Never of its row, its column, its section or its full width, so moving a widget does not change it. Send it back as if_widget_hash. Null when the widget carries no definition, which is also a widget /v1/widgets/update refuses.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "lost_properties": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What an update carrying the definition this widget already has would not be able to keep. /v1/capabilities lists the values under widgets.lost_properties."
          },
          "backup_keys": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WidgetBackup"
            },
            "description": "The backups of this widget, newest first. They expire after 7 days."
          }
        }
      },
      "WidgetsGetResponse": {
        "type": "object",
        "description": "One widget.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/WidgetsGetData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "WidgetsUpdateRequest": {
        "type": "object",
        "description": "Which widget to rewrite and what it should show, or what its AI summary should read.",
        "required": [
          "report_key",
          "section_key",
          "widget_key"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report the widget belongs to."
          },
          "section_key": {
            "type": "string",
            "description": "The section the widget is in."
          },
          "widget_key": {
            "type": "string",
            "description": "The widget to rewrite."
          },
          "dsl": {
            "$ref": "#/components/schemas/WidgetDsl",
            "description": "The whole definition of the widget, not the part of it that changes: the widget is rebuilt from it. Start from the dsl /v1/widgets/get answered, change what you want and send it back. There is no integration_id here -- the integration a widget reads from cannot be changed -- and full_width is not read either, because an update never moves the widget. A body carries a dsl or an ai_summary, exactly one of the two."
          },
          "ai_summary": {
            "$ref": "#/components/schemas/WidgetAiSummary",
            "description": "For an AI summary, which has no definition: what the summary reads, the settings it is generated with and its brief. Everything of it is a partial write, the selection included: what the block does not name is left exactly as it is, so a call that changes a setting or the brief does not have to send `widget_keys` or `section_keys` back. Naming one of them replaces the selection, and naming an EMPTY one is the value 'reads nothing', with a SELECTION_EMPTY warning. The widget has to be a summary already; the scope cannot be changed and neither can its place, so `position` is refused here. Mutually exclusive with dsl."
          },
          "internal_notes": {
            "type": "string",
            "description": "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": {
            "type": "boolean",
            "description": "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": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          },
          "if_widget_hash": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "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."
          }
        },
        "additionalProperties": false
      },
      "WidgetsUpdateData": {
        "type": "object",
        "description": "The widget as it was left, the backup that undoes the call, and what the rewrite cost.",
        "required": [
          "report_key",
          "section_key",
          "widget_key",
          "widget",
          "widget_hash",
          "backup_key",
          "lost_properties",
          "affects_only_this_report",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "section_key": {
            "type": "string"
          },
          "widget_key": {
            "type": "string"
          },
          "widget": {
            "$ref": "#/components/schemas/ReportWidget",
            "description": "The widget as it is now, with the definition the platform really stored. It is not always the one that was sent: a chart drawn along a time dimension is stored as a trend, and a data view seeds the dimensions it needs."
          },
          "widget_hash": {
            "description": "The fingerprint of the widget after the write. Send it as if_widget_hash on the next one.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "backup_key": {
            "description": "The backup taken before the write, which /v1/widgets/restore puts back. Null in a rehearsal, which writes nothing and therefore backs up nothing.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "lost_properties": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "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": {
            "type": "boolean",
            "description": "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": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          },
          "ai_summary": {
            "$ref": "#/components/schemas/WidgetAiSummary",
            "description": "Present only when the write was about an AI summary: the scope it has, the selection it ended up with -- the one the body named, or the one the platform picked -- and what one generation of it costs. It confirms the SELECTION and not the whole block: the settings and the brief the call wrote are read back in widget.ai_summary."
          },
          "before": {
            "$ref": "#/components/schemas/WidgetAiSummary",
            "description": "Present only when the write was about an AI summary: the ai_summary block as it was before the call, brief included, which is the body that undoes it. A widget rewritten from a dsl has none: what undoes that is /v1/widgets/restore over the backup the same call took."
          }
        }
      },
      "WidgetsUpdateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "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.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/WidgetsUpdateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "WidgetsRestoreRequest": {
        "type": "object",
        "description": "Which widget to put back, and from which backup.",
        "required": [
          "report_key",
          "section_key",
          "widget_key",
          "backup_key"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "section_key": {
            "type": "string"
          },
          "widget_key": {
            "type": "string",
            "description": "The widget to put back. The backup has to be one of ITS backups."
          },
          "backup_key": {
            "type": "string",
            "description": "From the backup_keys of /v1/widgets/get, or from the backup_key an update answered. A backup of another widget or of another account is HTTP 404 NOT_FOUND."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          },
          "if_widget_hash": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "The widget_hash /v1/widgets/get published for this widget. The call is refused when the widget changed in between, so a restore never undoes an edit nobody has seen. 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."
          }
        },
        "additionalProperties": false
      },
      "WidgetsRestoreData": {
        "type": "object",
        "description": "The widget as the backup left it, and the backup taken of what it was before the restore.",
        "required": [
          "report_key",
          "section_key",
          "widget_key",
          "widget",
          "widget_hash",
          "backup_key",
          "lost_properties",
          "affects_only_this_report",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "section_key": {
            "type": "string"
          },
          "widget_key": {
            "type": "string"
          },
          "widget": {
            "$ref": "#/components/schemas/ReportWidget",
            "description": "The widget as it is now, with the definition the platform really stored. It is not always the one that was sent: a chart drawn along a time dimension is stored as a trend, and a data view seeds the dimensions it needs."
          },
          "widget_hash": {
            "description": "The fingerprint of the widget after the write. Send it as if_widget_hash on the next one.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "backup_key": {
            "description": "The backup taken before the write, which /v1/widgets/restore puts back. Null in a rehearsal, which writes nothing and therefore backs up nothing.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "lost_properties": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "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": {
            "type": "boolean",
            "description": "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": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "WidgetsRestoreResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "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.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/WidgetsRestoreData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "WidgetAiSummary": {
        "type": "object",
        "description": "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.",
        "required": [
          "scope"
        ],
        "properties": {
          "scope": {
            "type": "string",
            "description": "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.",
            "enum": [
              "widget",
              "section",
              "report"
            ]
          },
          "widget_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "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": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "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": {
            "type": "string",
            "description": "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.",
            "enum": [
              "first",
              "last"
            ],
            "writeOnly": true
          },
          "ai_prompt": {
            "maxLength": 2000,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "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": {
            "type": "boolean",
            "description": "Whether the summary is given the period before to compare against. True unless it was turned off."
          },
          "ai_auto_refresh": {
            "type": "boolean",
            "description": "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": {
            "type": "string",
            "description": "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.",
            "enum": [
              "omit",
              "default_text",
              "last_cached"
            ]
          },
          "ai_quota_fallback_policy": {
            "type": "string",
            "description": "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.",
            "enum": [
              "omit",
              "default_text",
              "last_cached"
            ]
          },
          "ai_fallback_text": {
            "maxLength": 2000,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "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": {
            "type": "boolean",
            "description": "Whether the summary carries the line saying it was written by AI. True unless it was turned off."
          },
          "ai_brief": {
            "maxLength": 2000,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "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": {
            "type": "integer",
            "description": "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.",
            "readOnly": true
          },
          "credits_note": {
            "type": "string",
            "description": "When those credits are spent.",
            "readOnly": true
          }
        },
        "additionalProperties": false
      },
      "WidgetsRenameRequest": {
        "type": "object",
        "description": "Which widget to rename, and what to call it.",
        "required": [
          "report_key",
          "section_key",
          "widget_key",
          "name"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report the widget belongs to."
          },
          "section_key": {
            "type": "string",
            "description": "The section the widget is in."
          },
          "widget_key": {
            "type": "string",
            "description": "The widget to rename."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80,
            "description": "The new title, between 1 and 80 characters, plain text. A character the datastore cannot hold -- an emoji -- is refused with INVALID_VALUE, the same way the application refuses it."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          },
          "if_widget_hash": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "The widget_hash /v1/widgets/get published for this widget. A stale one is HTTP 409 WIDGET_CHANGED; read the widget again and retry with the new hash AND THE SAME idempotency_key."
          }
        },
        "additionalProperties": false
      },
      "WidgetsRenameData": {
        "type": "object",
        "description": "The widget as it was left, with its new title.",
        "required": [
          "report_key",
          "section_key",
          "widget_key",
          "widget",
          "widget_hash",
          "affects_only_this_report",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "section_key": {
            "type": "string"
          },
          "widget_key": {
            "type": "string"
          },
          "widget": {
            "$ref": "#/components/schemas/ReportWidget",
            "description": "The widget as a read publishes it, carrying the name it now has."
          },
          "widget_hash": {
            "description": "The fingerprint of the widget, unchanged by the rename: the name is not part of what it covers. Null for a widget the DSL cannot describe and that is not an AI summary either.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "affects_only_this_report": {
            "type": "boolean",
            "description": "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": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "WidgetsRenameResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/WidgetsRenameData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "WidgetsSetConnectionRequest": {
        "type": "object",
        "description": "Which widget to rebind, and which connection and account it reads through from now on.",
        "required": [
          "report_key",
          "section_key",
          "widget_key",
          "connection_key",
          "account_id"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report the widget belongs to. Its report_level_data_sources has to be false: a report that binds once for the whole report is rebound with the data_sources of /v1/reports/update."
          },
          "section_key": {
            "type": "string",
            "description": "The section the widget is in."
          },
          "widget_key": {
            "type": "string",
            "description": "The widget to rebind."
          },
          "connection_key": {
            "type": "string",
            "description": "The connection the widget reads through, from /v1/connections. It has to be one of the integration the widget already reads: the source of a widget that exists never changes."
          },
          "account_id": {
            "type": "string",
            "description": "The account within that connection, checked against the accounts /v1/connections publishes for it."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          },
          "if_widget_hash": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "The widget_hash /v1/widgets/get published for this widget. A stale one is HTTP 409 WIDGET_CHANGED; read the widget again and retry with the new hash AND THE SAME idempotency_key."
          }
        },
        "additionalProperties": false
      },
      "WidgetsSetConnectionData": {
        "type": "object",
        "description": "The widget as it was left, reading its new account, and the backup that undoes the call.",
        "required": [
          "report_key",
          "section_key",
          "widget_key",
          "widget",
          "widget_hash",
          "backup_key",
          "lost_properties",
          "affects_only_this_report",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "section_key": {
            "type": "string"
          },
          "widget_key": {
            "type": "string"
          },
          "widget": {
            "$ref": "#/components/schemas/ReportWidget",
            "description": "The widget as a read publishes it, with the connection_key and the account_id it now reads through."
          },
          "widget_hash": {
            "description": "The fingerprint of the widget after the write. The connection is part of what it covers, so it changes with this call.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "backup_key": {
            "description": "The backup taken before the write, which /v1/widgets/restore puts back. A restore puts back the DEFINITION and leaves the connection the widget has at that moment, so the undo of this call is another call to it. Null in a rehearsal.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "lost_properties": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Empty: nothing of the widget but its connection is touched."
          },
          "affects_only_this_report": {
            "type": "boolean",
            "description": "Always false: a report built on a template does not take this call."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "WidgetsSetConnectionResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/WidgetsSetConnectionData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "ReportsUpdateRequest": {
        "type": "object",
        "description": "Which report to change, and which of its settings. Everything the body does not carry is left exactly as it is.",
        "required": [
          "report_key"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report to change, from /v1/reports/list."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60,
            "description": "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": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "description": "The name the list of the application shows, 1 to 100 characters. Left out, the one the report has is kept."
          },
          "subtitle": {
            "type": "string",
            "maxLength": 60,
            "description": "The subtitle under the title. An empty string clears it."
          },
          "internal_notes": {
            "type": "string",
            "maxLength": 250,
            "description": "Notes only the account sees. An empty string clears them."
          },
          "share": {
            "type": "object",
            "description": "The public /share link of the report. On a report that has NO link yet the block has to carry `enabled`: the id is minted by this call, and a body that only set the other fields would publish a report nobody asked to publish. Refused on a template, which has no public link.",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Whether the link answers. Defaults to true the first time the report is shared."
              },
              "password": {
                "description": "The password the link asks for, stored hashed. null removes it and leaves the link unprotected; an empty string is refused; leaving the field out keeps the password the link already had.",
                "anyOf": [
                  {
                    "type": "string",
                    "minLength": 1
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "allow_date_range": {
                "type": "boolean",
                "description": "Whether whoever opens the link may change the period. Defaults to false."
              },
              "regenerate": {
                "type": "boolean",
                "description": "Mint a new id, which invalidates the link that was given out. Default false."
              }
            },
            "additionalProperties": false
          },
          "data_sources": {
            "type": "array",
            "description": "The report level bindings, one entry per integration, as /v1/reports/create takes them. Refused on a report that assigns a connection per widget.",
            "items": {
              "$ref": "#/components/schemas/DataSourceInput"
            }
          },
          "date_range": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CompositionDateRange"
              }
            ],
            "description": "The period EVERY section of the report is moved to. It writes the range and nothing else -- the flags of each section header are left as they are -- but the period each section is COMPARED against travels inside the range and is recomputed from the length of the new one. It is not atomic: the sections are written one after another, and the answer lists the ones it wrote. The cached results of the report are dropped. On a TEMPLATE it reaches every report built on it; on a report built on a template it is refused."
          },
          "brand_template": {
            "type": "string",
            "description": "The brand template the report is printed with: a key of a brand template of the account, or an empty string for the system one. A template carries no brand of its own and takes only the empty string, or the key it still stores, which changes nothing."
          },
          "brand_template_name": {
            "type": "string",
            "readOnly": true,
            "description": "Published by /v1/reports/get and written by nothing. It is accepted here so the whole `settings` block a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "front_cover": {
            "$ref": "#/components/schemas/ReportFrontCoverInput",
            "description": "The front cover of the report: its type, its texts and its date line. The uploaded PDF itself is not written by this call."
          },
          "pdf": {
            "$ref": "#/components/schemas/ReportPdfOptions",
            "description": "What the generated PDF carries besides the report itself."
          },
          "localization": {
            "$ref": "#/components/schemas/ReportLocalizationInput",
            "description": "A report either follows the localization of the ACCOUNT or carries its own, which is what `use` says. The six settings below exist only in the second case: sending one of them with `use: account` is refused instead of being ignored, and a call that WRITES this branch asks for the six together. Once they are all there each of them can be written on its own, and a block that writes none of them changes nothing. The time zone and the time format are not settings of a report: they are the account's."
          },
          "language_file": {
            "type": "string",
            "description": "The language file the report is printed with: an empty string for the system default, one of the sixteen system files (`language_file_en`, `language_file_es`, ...) or the key of a language file of the account."
          },
          "language_file_name": {
            "type": "string",
            "readOnly": true,
            "description": "Published by /v1/reports/get and written by nothing. It is accepted here so the whole `settings` block a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "substitution_variables": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 800
            },
            "minItems": 5,
            "maxItems": 5,
            "description": "The five substitution variables of the report, positional: the list carries exactly five values, one per %%CUSTOM_VAR_n%%, and an unused one is an empty string. Each of them is at most 800 characters."
          },
          "widgets_show": {
            "$ref": "#/components/schemas/ReportWidgetsShow",
            "description": "What the header of every widget of the report shows."
          },
          "snapshots_with_scheduled_executions": {
            "type": "boolean",
            "description": "Whether a snapshot of the report is stored every time it is delivered by a schedule."
          },
          "ai_brief": {
            "maxLength": 2000,
            "description": "What the AI summaries of this report should know about the client. Three states: leaving the field out -- or sending null, which is what a read answers for a report that has none -- keeps the brief the report has, an empty string removes it, and a text replaces it. At most 2000 characters. A brief that could not be written comes back as an AI_BRIEF_NOT_SAVED warning of a 200, with `ai_brief` left out of `updated_fields`.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          }
        },
        "additionalProperties": false
      },
      "ReportsUpdateData": {
        "type": "object",
        "description": "The report as a read publishes it, and what this call changed.",
        "required": [
          "report",
          "updated_fields",
          "before",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report": {
            "$ref": "#/components/schemas/ReportHeader",
            "description": "The header of the report after the write, in the shape /v1/reports/get publishes it."
          },
          "updated_fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The fields of the body this call wrote, in the order it read them."
          },
          "before": {
            "type": "object",
            "additionalProperties": true,
            "description": "The previous value of every field this call touched, which is the body that undoes it. date_range is not in it: the period belongs to each section and there is no single previous value to give back."
          },
          "sections_updated": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Present only when the body carried a date_range: the keys of the sections that were moved to it."
          },
          "atomic": {
            "type": "boolean",
            "description": "Present with sections_updated, and always false: the sections are written one after another with no transaction across them."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "ReportsUpdateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/ReportsUpdateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "ReportsSetConnectionsRequest": {
        "type": "object",
        "description": "Which report to rebind, and which connection and account each of its integrations reads from now on.",
        "required": [
          "report_key",
          "connections"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report to rebind. Its report_level_data_sources has to be false."
          },
          "connections": {
            "type": "array",
            "minItems": 1,
            "description": "One entry per integration. Every widget of the report that reads that integration is pointed at the pair; an integration named twice is refused with ONE_CONNECTION_PER_INTEGRATION.",
            "items": {
              "type": "object",
              "required": [
                "integration_id",
                "connection_key",
                "account_id"
              ],
              "properties": {
                "integration_id": {
                  "type": "string",
                  "description": "From /v1/integrations."
                },
                "connection_key": {
                  "type": "string",
                  "description": "From /v1/connections, of that integration."
                },
                "account_id": {
                  "type": "string",
                  "description": "An account within that connection."
                }
              },
              "additionalProperties": false
            }
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          }
        },
        "additionalProperties": false
      },
      "ReportsSetConnectionsData": {
        "type": "object",
        "description": "Which widgets were bound and which were not, widget by widget.",
        "required": [
          "report_key",
          "bound",
          "unbound",
          "ignored",
          "atomic",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "bound": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "One widget of the report and the connection it reads through.",
              "properties": {
                "widget_key": {
                  "type": "string"
                },
                "section_key": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "integration_id": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "connection_key": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "account_id": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "backup_key": {
                  "description": "The backup taken before the write, which /v1/widgets/restore puts back. Null in a rehearsal.",
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              }
            },
            "description": "The widgets that now read the connection the body named."
          },
          "unbound": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "One widget that reads data and was not rebound, with the reason.",
              "properties": {
                "widget_key": {
                  "type": "string"
                },
                "section_key": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "integration_id": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "reason": {
                  "type": "string",
                  "enum": [
                    "integration_not_in_request",
                    "multi_account",
                    "accounts_group",
                    "write_failed"
                  ],
                  "description": "integration_not_in_request when the body names no connection for its integration, multi_account when it reads several connected accounts at once, accounts_group when it is a cross-platform widget that reads an accounts group, whose connections it keeps, write_failed when the write itself did not go through. A widget of the last kind carries no backup_key: the backup it took undoes nothing."
                }
              }
            },
            "description": "The widgets that read data and were left alone, each with its reason. A widget that reads no data at all is in neither list."
          },
          "ignored": {
            "type": "array",
            "description": "Entries of connections[] that no widget of this report reads through. They are resolved all the same -- a connection of another account is refused -- and reported here so a client that named the wrong integration is not answered a silent 200.",
            "items": {
              "type": "object",
              "properties": {
                "integration_id": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "connection_key": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "account_id": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "reason": {
                  "type": "string",
                  "enum": [
                    "no_widget_reads_this_integration"
                  ]
                }
              }
            }
          },
          "atomic": {
            "type": "boolean",
            "description": "Always false: each widget is written on its own, and retrying the same body finishes the job."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "ReportsSetConnectionsResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/ReportsSetConnectionsData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "ReportsCopyRequest": {
        "type": "object",
        "description": "Which report to copy, under which name, and as what.",
        "required": [
          "report_key",
          "name"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report to copy, from /v1/reports/list or /v1/templates/list."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60,
            "description": "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": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "description": "The name the list of the application shows, 1 to 100 characters. Left out, it is the title."
          },
          "as": {
            "type": "string",
            "enum": [
              "report",
              "template"
            ],
            "description": "What the copy is. It defaults to whatever the source is: a report copied as a report, a template copied as a template. Turning a report into a template is refused on a report built on a template and on one that assigns a connection per widget, and turning a template into a report is refused outright -- that is a materialization and not a copy."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          }
        },
        "additionalProperties": false
      },
      "ReportsCopyData": {
        "type": "object",
        "description": "The copy that was written.",
        "required": [
          "report_key",
          "app_url",
          "source_report_key",
          "source_type",
          "name",
          "type",
          "created_via",
          "created_keys",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The key of the copy."
          },
          "app_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "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."
          },
          "source_report_key": {
            "type": "string"
          },
          "source_type": {
            "type": "string",
            "enum": [
              "report",
              "template",
              "template_based"
            ],
            "description": "What the source was."
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "report",
              "template"
            ],
            "description": "What the copy is. A source built on a template answers report: the copy is a regular report of its own."
          },
          "created_via": {
            "type": "string",
            "enum": [
              "api"
            ]
          },
          "created_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Every key this call created, flat, for the retry that reads a reduced idempotency record."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. A source built on a template carries one: the copy does not follow that template any more."
          }
        }
      },
      "ReportsCopyResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/ReportsCopyData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "ReportsDuplicateForClientRequest": {
        "type": "object",
        "description": "Which report to duplicate, under which name, pointed at whose data, and what to do with the copy afterwards.",
        "required": [
          "report_key",
          "name"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report to duplicate. A template is refused: build a report from it instead."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60,
            "description": "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": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "description": "The name the list of the application shows, 1 to 100 characters. Left out, it is the title."
          },
          "data_sources": {
            "type": "array",
            "description": "The rebinding of a source whose report_level_data_sources is TRUE. Refused when the source binds per widget.",
            "items": {
              "$ref": "#/components/schemas/DataSourceInput"
            }
          },
          "connections": {
            "type": "array",
            "description": "The rebinding of a source whose report_level_data_sources is FALSE, one entry per integration. Refused when the source binds once for the whole report.",
            "items": {
              "type": "object",
              "required": [
                "integration_id",
                "connection_key",
                "account_id"
              ],
              "properties": {
                "integration_id": {
                  "type": "string",
                  "description": "From /v1/integrations."
                },
                "connection_key": {
                  "type": "string",
                  "description": "From /v1/connections, of that integration."
                },
                "account_id": {
                  "type": "string",
                  "description": "An account within that connection."
                }
              },
              "additionalProperties": false
            },
            "minItems": 1
          },
          "schedule": {
            "type": "object",
            "description": "A body of /v1/schedules/create without its report_key, which is the copy's. Sending it makes the call ask for a confirmation and spends one unit of the daily email budget.",
            "additionalProperties": true
          },
          "portal_view_key": {
            "type": "string",
            "description": "The portal view the copy is added to, from /v1/portal-views/list. There is no 'same as the original': the key is explicit. Sending it makes the call ask for a confirmation."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          },
          "confirmation_token": {
            "type": "string",
            "description": "The token the first step of the call answered with. Send it back with the SAME body and the SAME idempotency_key to execute the call. It is good for one use and expires 600 seconds after it was issued."
          }
        },
        "additionalProperties": false
      },
      "ReportsDuplicateForClientData": {
        "type": "object",
        "description": "The copy that was written and how each of the steps after it went.",
        "required": [
          "report_key",
          "app_url",
          "source_report_key",
          "name",
          "type",
          "created_via",
          "bound",
          "failed",
          "schedule",
          "portal_view",
          "atomic",
          "created_keys",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The key of the copy. It exists whatever the other fields say."
          },
          "app_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The link that opens the copy in the Reporting Ninja application. 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. Null in the answer of a call that broke before the copy was written."
          },
          "source_report_key": {
            "type": "string"
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "report"
            ]
          },
          "created_via": {
            "type": "string",
            "enum": [
              "api"
            ]
          },
          "bound": {
            "type": "array",
            "description": "What the copy now reads: one entry per report level binding, or one per widget when the source binds per widget. The per widget entries carry no backup_key: the widgets were created by this call and the inverse of the whole thing is deleting the copy.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "failed": {
            "type": "array",
            "description": "What could not be bound, each entry with its reason. Empty when everything was bound.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "schedule": {
            "type": "object",
            "description": "Whether a schedule was asked for and whether it was created.",
            "required": [
              "requested",
              "created"
            ],
            "properties": {
              "requested": {
                "type": "boolean"
              },
              "created": {
                "type": "boolean"
              },
              "schedule_key": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "error": {
                "type": "object",
                "description": "Present when it was asked for and not created.",
                "properties": {
                  "error_code": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "message": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                }
              }
            }
          },
          "portal_view": {
            "type": "object",
            "description": "Whether a portal view was named and whether the copy was added to it.",
            "required": [
              "requested",
              "added"
            ],
            "properties": {
              "requested": {
                "type": "boolean"
              },
              "added": {
                "type": "boolean"
              },
              "portal_view_key": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "error": {
                "type": "object",
                "description": "Present when it was named and the copy was not added.",
                "properties": {
                  "error_code": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "message": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                }
              }
            }
          },
          "atomic": {
            "type": "boolean",
            "description": "Always false: only the copy itself can refuse the call, and the three steps after it are reported as they went."
          },
          "created_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Every key this call created, flat, for the retry that reads a reduced idempotency record."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "ReportsDuplicateForClientResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/ReportsDuplicateForClientData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "ReportsSetCoverFileRequest": {
        "type": "object",
        "description": "The report whose front cover file is written, and the file itself or null to remove the one it carries.",
        "required": [
          "report_key",
          "file"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report, from /v1/reports/list or /v1/reports/get."
          },
          "file": {
            "description": "The PDF the cover prints, or null to remove the one the report carries. Sending null also moves the cover type back to the one that prints no uploaded file.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/ReportCoverFileInput"
              },
              {
                "type": "null"
              }
            ]
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          }
        },
        "additionalProperties": false
      },
      "ReportsSetCoverFileData": {
        "type": "object",
        "description": "The front cover the report carries now, and what it carried before.",
        "required": [
          "report_key",
          "front_cover",
          "updated_fields",
          "before",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "front_cover": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ReportFrontCover"
              }
            ],
            "description": "The front cover after the write, in the shape /v1/reports/get publishes it under settings: it can be sent straight back to /v1/reports/update, where the file key is read only and ignored."
          },
          "updated_fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What the call really changed: `front_cover.file` whenever a file was stored or taken away -- a removal asked of a report that carries none changes nothing -- and `front_cover.type` when the file moved the type."
          },
          "before": {
            "type": "object",
            "description": "The front cover type the report had and the file it carried, by name. The file itself is not kept anywhere, so this is all there is of it.",
            "required": [
              "type",
              "file"
            ],
            "properties": {
              "type": {
                "type": "string",
                "description": "The front cover type before the write."
              },
              "file": {
                "description": "The file the report carried, by name, or null when it carried none.",
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/ReportCoverFile"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors. COVER_TYPE_SET travels here when the call moved the front cover type."
          }
        }
      },
      "ReportsSetCoverFileResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/ReportsSetCoverFileData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "WidgetPlacement": {
        "type": "object",
        "description": "One widget and where it goes.",
        "required": [
          "widget_key",
          "row",
          "column"
        ],
        "properties": {
          "widget_key": {
            "type": "string",
            "description": "The widget to place, from /v1/reports/get."
          },
          "section_key": {
            "type": "string",
            "description": "The section it lands in. Omitted in a request, the widget stays in the section it is in; previous_layout always names it."
          },
          "row": {
            "type": "integer",
            "minimum": 1,
            "description": "The row of that section, counting from 1."
          },
          "column": {
            "type": "integer",
            "minimum": 1,
            "description": "The column inside that row, counting from 1."
          }
        },
        "additionalProperties": false
      },
      "WidgetsMoveRequest": {
        "type": "object",
        "description": "Which widget or widgets to move, and where each of them goes. Exactly one of `to` and `layout` is sent.",
        "required": [
          "report_key"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report the widgets belong to."
          },
          "widget_key": {
            "type": "string",
            "description": "The widget `to` moves. It is sent with `to` and refused with `layout`, whose entries name their own widget."
          },
          "to": {
            "type": "object",
            "description": "Where that one widget goes. A row past the last one of the section opens a new row; without a column the widget goes at the end of the row it lands in.",
            "required": [
              "row"
            ],
            "properties": {
              "section_key": {
                "type": "string",
                "description": "The section it lands in. Omitted, it stays in the one it is in."
              },
              "row": {
                "type": "integer",
                "minimum": 1
              },
              "column": {
                "type": "integer",
                "minimum": 1
              }
            },
            "additionalProperties": false
          },
          "layout": {
            "type": "array",
            "description": "Several widgets placed in one call, which is how the previous_layout of an earlier move is replayed. Every widget of the sections it touches that is not named here keeps the place it has.",
            "minItems": 1,
            "maxItems": 60,
            "items": {
              "$ref": "#/components/schemas/WidgetPlacement"
            }
          },
          "if_position": {
            "type": "object",
            "description": "Where the widget of `to` was when the caller read it. Every field is optional and what is sent is what is compared; a widget that is somewhere else is HTTP 409 POSITION_CHANGED, with details.stored carrying where it really is. It guards the ONE widget `to` names, so it is refused with INVALID_VALUE on a body that carries `layout`, whatever its size. Like if_widget_hash it is outside what the idempotency_key identifies, so the retry reuses the same key.",
            "properties": {
              "section_key": {
                "type": "string"
              },
              "row": {
                "type": "integer"
              },
              "column": {
                "type": "integer"
              }
            },
            "additionalProperties": false
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          }
        },
        "additionalProperties": false
      },
      "WidgetsMoveData": {
        "type": "object",
        "description": "Where the widgets ended up, and where everything was before the call.",
        "required": [
          "report_key",
          "moved",
          "previous_layout",
          "sections_touched",
          "atomic",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "moved": {
            "type": "array",
            "description": "One entry per widget the call named, with the place it has now.",
            "items": {
              "type": "object",
              "required": [
                "widget_key",
                "section_key",
                "row",
                "column",
                "full_width",
                "section_changed"
              ],
              "properties": {
                "widget_key": {
                  "type": "string"
                },
                "section_key": {
                  "type": "string"
                },
                "row": {
                  "description": "",
                  "anyOf": [
                    {
                      "type": "integer"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "column": {
                  "description": "",
                  "anyOf": [
                    {
                      "type": "integer"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "full_width": {
                  "type": "boolean",
                  "description": "False once the widget shares its row: a row with company is no longer a row of one."
                },
                "section_changed": {
                  "type": "boolean",
                  "description": "True when the widget ended up in another section, which is when its cached results are dropped."
                }
              }
            }
          },
          "previous_layout": {
            "type": "array",
            "description": "Where every widget of every touched section was BEFORE this call, in the very shape `layout` takes: sending this array back as the `layout` of another call puts the report exactly as it was, and that is the inverse of a move. A widget whose row or column was never written is left out -- a layout names cells, and it had none to go back to.",
            "items": {
              "$ref": "#/components/schemas/WidgetPlacement"
            }
          },
          "sections_touched": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The sections read and rewritten by the call."
          },
          "atomic": {
            "type": "boolean",
            "description": "Always false: the sections are renumbered widget by widget and not inside one transaction. The inverse of a partial move is previous_layout."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "WidgetsMoveResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/WidgetsMoveData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "WidgetsCopyRequest": {
        "type": "object",
        "description": "Which widget to copy and where the copy goes.",
        "required": [
          "report_key",
          "widget_key"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report the widget belongs to."
          },
          "widget_key": {
            "type": "string",
            "description": "The widget to copy."
          },
          "to": {
            "type": "object",
            "description": "Where the copy goes. Omitted, it opens the row right below the original, which is what the application does.",
            "properties": {
              "section_key": {
                "type": "string",
                "description": "The section the copy lands in. Omitted, it is the one the original is in."
              },
              "row": {
                "type": "integer",
                "minimum": 1,
                "description": "The row the copy opens: that row and everything under it move one down."
              }
            },
            "additionalProperties": false
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          }
        },
        "additionalProperties": false
      },
      "WidgetsCopyData": {
        "type": "object",
        "description": "The copy, as a read publishes it.",
        "required": [
          "report_key",
          "section_key",
          "source_widget_key",
          "widget_key",
          "widget",
          "atomic",
          "lost_selections",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "section_key": {
            "type": "string",
            "description": "The section the copy landed in."
          },
          "source_widget_key": {
            "type": "string",
            "description": "The widget it was copied from."
          },
          "widget_key": {
            "description": "The key of the copy. Null in the answer of a dry run, where nothing was created.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "widget": {
            "$ref": "#/components/schemas/ReportWidget",
            "description": "The copy in the shape every read publishes a widget in."
          },
          "lost_selections": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What the copy could not carry over. Empty in every ordinary copy: only an AI summary copied into ANOTHER section fills it, with the keys of the widgets it used to read and no longer can, because they are not in the section it landed in."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          },
          "atomic": {
            "type": "boolean",
            "description": "Always false: the row is created, the AI selections of a copied summary are remapped, the section is renumbered around the copy and the client brief is copied, and those are four writes and not one transaction. The inverse of a partial copy is deleting the copy."
          }
        }
      },
      "WidgetsCopyResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/WidgetsCopyData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "SectionsReorderRequest": {
        "type": "object",
        "description": "The sections of a report in the order they are to be read.",
        "required": [
          "report_key",
          "section_keys"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report to reorder."
          },
          "section_keys": {
            "type": "array",
            "description": "The SAME section keys /v1/reports/get publishes for the report, each of them once, in the new order. A list that adds, drops or repeats a key is refused.",
            "minItems": 1,
            "items": {
              "type": "string"
            }
          },
          "if_version": {
            "type": "integer",
            "description": "The sections version of the report, as /v1/reports/get publishes it. A stale one is HTTP 409 VERSION_MISMATCH, compared again inside the transaction that writes."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          }
        },
        "additionalProperties": false
      },
      "SectionsReorderData": {
        "type": "object",
        "description": "The order the report is in now, and the version it reached.",
        "required": [
          "report_key",
          "version",
          "section_keys",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "version": {
            "type": "integer",
            "description": "The sections version after the write. Every write that touches the list of sections raises it."
          },
          "section_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The sections in the order they are read in."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "SectionsReorderResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/SectionsReorderData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "SectionState": {
        "type": "object",
        "description": "One section as a read publishes it, without its widgets: the same keys /v1/reports/get answers inside sections[].",
        "required": [
          "section_key",
          "name",
          "show_previous_period",
          "add_summary",
          "show_date_range",
          "page_orientation",
          "page_break"
        ],
        "properties": {
          "section_key": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "show_previous_period": {
            "type": "boolean",
            "description": "True when the header of the section names the period it is compared against, under its date range. The same key a section of /v1/reports/create takes."
          },
          "add_summary": {
            "description": "The AI summary OF THIS SECTION, said the way `add_summary` asks for one: `null` when the section carries none, `{\"position\":\"first\"}` or `{\"position\":\"last\"}` when it is in the first or the last row of the section, and `true` when it carries one placed by hand somewhere in between, which no position of the composition describes (writing that report back with `true` puts it in the first row). A summary of the WHOLE report hosted by this section is NOT answered here: it shows up as one more widget of type `ai_summary`, and the `add_summary` at the root of a composition is what asks for it.",
            "anyOf": [
              {
                "type": "null"
              },
              {
                "type": "boolean"
              },
              {
                "type": "object",
                "required": [
                  "position"
                ],
                "properties": {
                  "position": {
                    "type": "string",
                    "enum": [
                      "first",
                      "last"
                    ]
                  }
                },
                "additionalProperties": false
              }
            ]
          },
          "show_date_range": {
            "type": "boolean",
            "description": "Whether the header of the section shows the period it covers."
          },
          "page_orientation": {
            "description": "How the pages of this section are laid out in the PDF, or null when the section holds no value for it.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "portrait",
                  "landscape"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "page_break": {
            "type": "boolean",
            "description": "Whether the PDF starts a new page at this section."
          }
        }
      },
      "WidgetImage": {
        "type": "object",
        "description": "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.",
        "required": [
          "width",
          "height",
          "bytes"
        ],
        "properties": {
          "width": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "height": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "bytes": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "description": "How much the stored file weighs."
          }
        }
      },
      "SectionsCreateRequest": {
        "type": "object",
        "description": "The report the section is added to, and the two things a new section is asked for.",
        "required": [
          "report_key",
          "name"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report the section goes into."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80,
            "description": "The title of the section, 1 to 80 characters, with no character above the Basic Multilingual Plane (an emoji, most often)."
          },
          "page_break": {
            "type": "boolean",
            "description": "Whether the PDF starts a new page at this section. Default true, which is what the application proposes for a new section."
          },
          "if_version": {
            "type": "integer",
            "description": "The sections version of the report, as /v1/reports/get publishes it. A stale one is HTTP 409 VERSION_MISMATCH."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          }
        },
        "additionalProperties": false
      },
      "SectionsCreateData": {
        "type": "object",
        "description": "The section that was created and the version the report reached.",
        "required": [
          "report_key",
          "section",
          "version",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "section": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SectionState"
              }
            ],
            "description": "The section as a read publishes it. section_key is null in the answer of a dry run, where nothing was created."
          },
          "version": {
            "type": "integer",
            "description": "The sections version after the write."
          },
          "created_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The key of the section that was created. Absent from a dry run."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "SectionsCreateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/SectionsCreateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "SectionsUpdateRequest": {
        "type": "object",
        "description": "Which section to change, and which of its settings. Everything the body does not carry is left exactly as it is, the temporary filter a client portal may be reading the section through included.",
        "required": [
          "report_key",
          "section_key"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report the section belongs to."
          },
          "section_key": {
            "type": "string",
            "description": "The section to change."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80,
            "description": "The title of the section, 1 to 80 characters, with no character above the Basic Multilingual Plane."
          },
          "show_date_range": {
            "type": "boolean",
            "description": "Whether the header of the section shows the period it covers."
          },
          "show_previous_period": {
            "type": "boolean",
            "description": "Whether the header also names the period the section is compared against. It is drawn UNDER the date range, so with show_date_range false it is written as false and the answer carries a warning."
          },
          "page_orientation": {
            "type": "string",
            "enum": [
              "portrait",
              "landscape"
            ],
            "description": "How the pages of this section are laid out in the PDF."
          },
          "page_break": {
            "type": "boolean",
            "description": "Whether the PDF starts a new page at this section."
          },
          "date_range": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CompositionDateRange"
              }
            ],
            "description": "The period THIS section reads, and with it every widget of the section that carries no period of its own. The cached results of those widgets are dropped. To move every section of the report at once, use the date_range of /v1/reports/update."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          }
        },
        "additionalProperties": false
      },
      "SectionsUpdateData": {
        "type": "object",
        "description": "The section as it is now, what the call changed, and what those fields were before.",
        "required": [
          "report_key",
          "section",
          "updated_fields",
          "before",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "section": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SectionState"
              }
            ],
            "description": "The section as a read publishes it, with the fields of the body already laid over it."
          },
          "updated_fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The fields this call wrote."
          },
          "before": {
            "type": "object",
            "description": "The previous value of every field the call touched, which is what an update that undoes it carries. date_range is NOT among them: the four values a body carries have no inverse reading of the stored range.",
            "additionalProperties": true
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "SectionsUpdateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/SectionsUpdateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "SectionsCopyRequest": {
        "type": "object",
        "description": "The section to copy, the report it comes from and the report it lands in.",
        "required": [
          "report_key",
          "source_report_key",
          "section_key"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report the copy lands in."
          },
          "source_report_key": {
            "type": "string",
            "description": "The report the section comes from, which is NOT the one the copy lands in."
          },
          "section_key": {
            "type": "string",
            "description": "The section of that report to copy."
          },
          "if_version": {
            "type": "integer",
            "description": "The sections version of the report, as /v1/reports/get publishes it. A stale one is HTTP 409 VERSION_MISMATCH."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          }
        },
        "additionalProperties": false
      },
      "SectionsCopyData": {
        "type": "object",
        "description": "The copy, the version the target report reached and what the copy could not carry.",
        "required": [
          "report_key",
          "source_report_key",
          "section",
          "version",
          "lost_selections",
          "widgets",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report the copy landed in."
          },
          "source_report_key": {
            "type": "string",
            "description": "The report it came from."
          },
          "section": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SectionState"
              }
            ],
            "description": "The copy as a read publishes it. section_key is null in the answer of a dry run, where nothing was created."
          },
          "version": {
            "type": "integer",
            "description": "The sections version of the TARGET report after the write."
          },
          "widgets": {
            "type": "integer",
            "description": "How many widgets travelled with the section."
          },
          "lost_selections": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The keys an AI summary inside the copied section could not carry: sections and widgets of the report it came from, which do not exist in the target. Empty when the section carries no summary or the summary read only what travelled with it."
          },
          "created_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The key of the section that was created. Absent from a dry run."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "SectionsCopyResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/SectionsCopyData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "WidgetsCreateImageRequest": {
        "type": "object",
        "description": "The picture, the section it goes in and where in that section.",
        "required": [
          "report_key",
          "section_key",
          "image_base64"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report the section belongs to."
          },
          "section_key": {
            "type": "string",
            "description": "The section the picture goes in."
          },
          "image_base64": {
            "type": "string",
            "description": "The file itself, base64 encoded, with no data URI prefix. At most 716800 bytes DECODED and at most 25000000 pixels, and it has to really be a JPEG, a PNG or a GIF: the first bytes of the file decide, never its name. The two dimensions are read off the header before anything is decoded, so an oversized picture is refused without being drawn."
          },
          "filename": {
            "type": "string",
            "description": "The name of the file the bytes came from, ending in .jpg, .jpeg, .png or .gif. Optional, and it decides nothing: what the bytes are is decided by the bytes."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80,
            "description": "What the widget is listed under, 1 to 80 characters. Nothing paints it beside the picture. LEAVE IT OUT to take the name a picture dropped in the application gets, in the language of the account; an empty string is refused rather than silently becoming that default."
          },
          "placement": {
            "type": "object",
            "description": "Where to put the picture. When absent, it is placed automatically. An image is not a KPI, so it counts as one of the elements a row holds. Only row and column are read here.",
            "required": [
              "row"
            ],
            "properties": {
              "row": {
                "type": "integer",
                "description": "1-based row. It must be an existing row or the first free one, and it must respect how that row is already filled: a refusal names the first free row in details.first_free_row."
              },
              "column": {
                "type": "integer",
                "description": "1-based column. A column that is taken is moved to the first free one and the answer says so with placement_adjusted."
              }
            },
            "additionalProperties": true
          },
          "if_version": {
            "type": "integer",
            "description": "The sections version of the report, as /v1/reports/get publishes it. A stale one is HTTP 409 VERSION_MISMATCH."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          }
        },
        "additionalProperties": false
      },
      "WidgetsCreateImageData": {
        "type": "object",
        "description": "The image widget that was created and what its picture measures.",
        "required": [
          "report_key",
          "section_key",
          "name",
          "row",
          "column",
          "placement_adjusted",
          "image",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "section_key": {
            "type": "string"
          },
          "widget_key": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The key of the widget. Absent from a dry run, where nothing was created."
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "row": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "column": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "placement_adjusted": {
            "type": "boolean",
            "description": "True when the column asked for was taken and the widget went to the next free one."
          },
          "image": {
            "allOf": [
              {
                "$ref": "#/components/schemas/WidgetImage"
              }
            ],
            "description": "The size read off the picture and how much the file weighs."
          },
          "created_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The key of the widget that was created. Absent from a dry run."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "WidgetsCreateImageResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/WidgetsCreateImageData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "WidgetsUpdateImageRequest": {
        "type": "object",
        "description": "The widget whose picture is replaced, and the new file.",
        "required": [
          "report_key",
          "section_key",
          "widget_key",
          "image_base64"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report the widget belongs to."
          },
          "section_key": {
            "type": "string",
            "description": "The section the widget is in."
          },
          "widget_key": {
            "type": "string",
            "description": "The image widget, from /v1/reports/get: one whose type is image."
          },
          "image_base64": {
            "type": "string",
            "description": "The file itself, base64 encoded, with no data URI prefix. At most 716800 bytes DECODED and at most 25000000 pixels, and it has to really be a JPEG, a PNG or a GIF: the first bytes of the file decide, never its name. The two dimensions are read off the header before anything is decoded, so an oversized picture is refused without being drawn."
          },
          "filename": {
            "type": "string",
            "description": "The name of the file the bytes came from, ending in .jpg, .jpeg, .png or .gif. Optional, and it decides nothing: what the bytes are is decided by the bytes."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          }
        },
        "additionalProperties": false
      },
      "WidgetsUpdateImageData": {
        "type": "object",
        "description": "The picture the widget carries now, and what the one it replaced measured.",
        "required": [
          "report_key",
          "section_key",
          "widget_key",
          "name",
          "image",
          "before",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "section_key": {
            "type": "string"
          },
          "widget_key": {
            "type": "string"
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "image": {
            "allOf": [
              {
                "$ref": "#/components/schemas/WidgetImage"
              }
            ],
            "description": "The size read off the new picture and how much its file weighs."
          },
          "before": {
            "type": "object",
            "description": "What the picture it replaced measured. The file itself is not kept anywhere, so this is all there is of it.",
            "required": [
              "image"
            ],
            "properties": {
              "image": {
                "$ref": "#/components/schemas/WidgetImage"
              }
            }
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "WidgetsUpdateImageResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/WidgetsUpdateImageData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "SectionsDuplicateRequest": {
        "type": "object",
        "description": "The section to copy where it already is.",
        "required": [
          "report_key",
          "section_key"
        ],
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report the section belongs to."
          },
          "section_key": {
            "type": "string",
            "description": "The section to duplicate."
          },
          "if_version": {
            "type": "integer",
            "description": "The sections version of the report, as /v1/reports/get publishes it. A stale one is HTTP 409 VERSION_MISMATCH."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate and answer what WOULD happen, without writing anything. Default false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "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.",
            "pattern": "^[A-Za-z0-9_.-]{1,128}$",
            "maxLength": 128
          }
        },
        "additionalProperties": false
      },
      "SectionsDuplicateData": {
        "type": "object",
        "description": "The duplicate and the version the report reached.",
        "required": [
          "report_key",
          "source_section_key",
          "section",
          "version",
          "widgets",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "source_section_key": {
            "type": "string",
            "description": "The section it was copied from."
          },
          "section": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SectionState"
              }
            ],
            "description": "The duplicate as a read publishes it. section_key is null in the answer of a dry run, where nothing was created, and `name` is already the one the copy will carry."
          },
          "version": {
            "type": "integer",
            "description": "The sections version after the write. A dry run does not answer it: the number the report is at right now is not the one this write produces."
          },
          "widgets": {
            "type": "integer",
            "description": "How many widgets travelled with the section."
          },
          "created_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The key of the section that was created. Absent from a dry run."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What was asked for and could not be expressed; the API returns an empty list."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            },
            "description": "Things worth knowing about a write that DID happen. They are never errors."
          }
        }
      },
      "SectionsDuplicateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/SectionsDuplicateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "ReportFrontCoverInput": {
        "type": "object",
        "description": "The front cover of the report. Every key is optional and what the body does not carry is left as it is; so is a key carrying null, which is what a read answers for a text the report does not have. The uploaded PDF itself is not written here: it has a route of its own, /v1/reports/set-cover-file.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "none",
              "report",
              "pdf",
              "pdf_and_report",
              "custom",
              "pdf_and_custom"
            ],
            "description": "What the front cover of the PDF is. `none` prints none, `report` draws the generated cover, `pdf` prints the uploaded file and `pdf_and_report` draws the generated text block over it. The `custom` pair is the legacy cover with texts of its own: it is kept and not offered, so only a report already printed with one of them is written with it again, and a cover that prints texts of its own asks for a title and a subtitle, in the body or already stored. The type commands the file: one that prints an uploaded PDF is refused on a report that carries none, and one that prints none removes the file the report carried, which the answer reports as a COVER_FILE_REMOVED warning."
          },
          "title": {
            "maxLength": 60,
            "description": "The title the generated cover prints, at most 60 characters. An empty string clears it; null leaves it as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "subtitle": {
            "maxLength": 60,
            "description": "The line under it, at most 60 characters. An empty string clears it; null leaves it as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "date_line": {
            "type": "string",
            "enum": [
              "none",
              "generation_date",
              "current_period",
              "current_and_past_period"
            ],
            "description": "The date line of the cover, of the hero of the public link and of the header of the client portal."
          },
          "file": {
            "readOnly": true,
            "description": "Published by a read and written by /v1/reports/set-cover-file and by nothing else: it is accepted here so the `front_cover` a read answers can be sent straight back, and the key is ignored, with a READ_ONLY_FIELD_IGNORED warning naming it.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/ReportCoverFile"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "additionalProperties": false
      },
      "ReportCoverFile": {
        "type": "object",
        "description": "The PDF file the front cover prints, by name. Its content is never published.",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "The name of the uploaded file."
          }
        }
      },
      "ReportCoverFileInput": {
        "type": "object",
        "description": "An uploaded front cover PDF: its name and its content.",
        "required": [
          "name",
          "content_base64"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "description": "The name the file is stored and shown under. It has to end in '.pdf' and carry no control character, because it is written into the header of the download of the file, and it is stored TRIMMED. It decides nothing else: what the bytes are is decided by the bytes."
          },
          "content_base64": {
            "type": "string",
            "description": "The file itself, base64 encoded, with no data URI prefix. At most 262144 bytes DECODED (524288 for the few accounts that were given the larger ceiling), and the first five bytes of the file have to be '%PDF-'."
          }
        },
        "additionalProperties": false
      },
      "ReportFrontCover": {
        "type": "object",
        "description": "The front cover of the report as a read publishes it, which is the object /v1/reports/update takes back: `file` is published and not written, and it is accepted and ignored so the object can be replayed as it is.",
        "required": [
          "type",
          "title",
          "subtitle",
          "date_line",
          "file"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "none",
              "report",
              "pdf",
              "pdf_and_report",
              "custom",
              "pdf_and_custom"
            ],
            "description": "What the front cover of the PDF is. `none` prints none, `report` draws the generated cover, `pdf` prints the uploaded file and `pdf_and_report` draws the generated text block over it. The `custom` pair is the legacy cover with texts of its own, kept and not offered: only a report already printed with one of them can be written with it again. The type commands the file: one that prints an uploaded PDF is refused on a report that carries none, and one that prints none removes the file the report carried, which the answer reports as a COVER_FILE_REMOVED warning."
          },
          "title": {
            "description": "The title the generated cover prints. An empty string is a title the report does not have; null is a column that was never written.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "subtitle": {
            "description": "The line under it. An empty string is a subtitle the report does not have; null is a column that was never written.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "date_line": {
            "type": "string",
            "enum": [
              "none",
              "generation_date",
              "current_period",
              "current_and_past_period"
            ],
            "description": "The date line of the cover, of the hero of the public link and of the header of the client portal."
          },
          "file": {
            "readOnly": true,
            "description": "The uploaded PDF the cover prints, by name, or null when the report carries none. Whether it is there is decided by the file itself and not by its name, so a legacy row whose name outlived its file answers null. It is written by /v1/reports/set-cover-file and by nothing else: a body that carries the key back here is accepted and the key ignored.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/ReportCoverFile"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "ReportPdfOptions": {
        "type": "object",
        "description": "What the generated PDF carries besides the report itself.",
        "properties": {
          "table_of_contents": {
            "type": "boolean",
            "description": "Whether the PDF opens with a table of contents."
          },
          "page_numbers": {
            "type": "boolean",
            "description": "Whether its pages are numbered."
          },
          "title_in_pdf": {
            "type": "boolean",
            "description": "Whether the title of the report is printed on each page."
          }
        },
        "additionalProperties": false
      },
      "ReportLocalizationInput": {
        "type": "object",
        "description": "A report either follows the localization of the ACCOUNT or carries its own, which is what `use` says. The six settings below exist only in the second case: sending one of them with `use: account` is refused instead of being ignored, and a call that WRITES this branch asks for the six together, in the body or already stored. Once they are all there each of them can be written on its own, and a block that gives no value at all changes nothing and asks for nothing -- which is what makes the block a read answers for a legacy report, whose six may be null, a body this call takes back. A key carrying null (or an empty string) gives no value and leaves that column exactly as it is; none of the six is ever written as null. The time zone and the time format are not settings of a report: they are the account's, and they are copied from it every time this branch is written.",
        "properties": {
          "use": {
            "type": "string",
            "enum": [
              "account",
              "report"
            ],
            "description": "Whether the report follows the localization of the account or carries its own."
          },
          "date_format": {
            "description": "How dates are printed: `DDMMYYYY` is dd/MM/yyyy, `MMDDYYYY` is MM/dd/yyyy and `YYYYMMDD` is yyyy/MM/dd.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "DDMMYYYY",
                  "MMDDYYYY",
                  "YYYYMMDD"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "week_starts_on": {
            "description": "The first day of a week, which decides how weekly series are bucketed.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "monday",
                  "sunday"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "decimal_separator": {
            "description": "The separator every figure of the report is printed with.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  ".",
                  ","
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "currency": {
            "minLength": 1,
            "maxLength": 3,
            "description": "The currency symbol printed beside monetary figures, 1 to 3 characters.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "currency_symbol_position": {
            "description": "Which side of the figure the symbol is printed on.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "left",
                  "right"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "pdf_page_size": {
            "description": "The page size of the generated PDF.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "A4",
                  "Letter"
                ]
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "additionalProperties": false
      },
      "ReportLocalization": {
        "type": "object",
        "description": "The localization of the report as a read publishes it. The six settings travel only when `use` is `report`: with `account` the report is printed with the account's, and they are the body that would put it back.",
        "required": [
          "use"
        ],
        "properties": {
          "use": {
            "type": "string",
            "enum": [
              "account",
              "report"
            ],
            "description": "Whether the report follows the localization of the account or carries its own."
          },
          "date_format": {
            "description": "How dates are printed.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "DDMMYYYY",
                  "MMDDYYYY",
                  "YYYYMMDD"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "week_starts_on": {
            "description": "The first day of a week.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "monday",
                  "sunday"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "decimal_separator": {
            "description": "The separator of every figure.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  ".",
                  ","
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "currency": {
            "description": "The currency symbol printed beside monetary figures.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "currency_symbol_position": {
            "description": "Which side of the figure the symbol is printed on.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "left",
                  "right"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "pdf_page_size": {
            "description": "The page size of the generated PDF.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "A4",
                  "Letter"
                ]
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "ReportWidgetsShow": {
        "type": "object",
        "description": "What the header of every widget of the report shows.",
        "properties": {
          "data_source_name": {
            "type": "boolean",
            "description": "Whether the name of the data source is shown."
          },
          "data_source_icon": {
            "type": "boolean",
            "description": "Whether its icon is shown."
          },
          "date_range": {
            "type": "boolean",
            "description": "Whether the period of the widget is shown."
          }
        },
        "additionalProperties": false
      },
      "ReportSettings": {
        "type": "object",
        "description": "The settings window of the report, in the same shape /v1/reports/update takes it: what a read answers here is what a write sends back, whole, with no key removed and no value rewritten. Three of its keys -- `brand_template_name`, `language_file_name` and `front_cover.file` -- are published here and written by nothing: that call accepts them, ignores them, and names each one in a READ_ONLY_FIELD_IGNORED warning. Everywhere else a null means the column has no value and a write that carries it back leaves that column exactly as it is.",
        "required": [
          "brand_template",
          "brand_template_name",
          "front_cover",
          "pdf",
          "localization",
          "language_file",
          "language_file_name",
          "substitution_variables",
          "widgets_show",
          "snapshots_with_scheduled_executions",
          "ai_brief"
        ],
        "properties": {
          "brand_template": {
            "type": "string",
            "description": "The brand template the report is printed with: a key of a brand template of the account, or an empty string for the system one."
          },
          "brand_template_name": {
            "readOnly": true,
            "description": "The name of that brand template, so naming one does not need a second call. Null when the report points at a brand template that no longer exists.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "front_cover": {
            "$ref": "#/components/schemas/ReportFrontCover"
          },
          "pdf": {
            "$ref": "#/components/schemas/ReportPdfOptions"
          },
          "localization": {
            "$ref": "#/components/schemas/ReportLocalization"
          },
          "language_file": {
            "type": "string",
            "description": "The language file the report is printed with: an empty string for the system default, one of the sixteen system files (`language_file_en`, `language_file_es`, ...) or the key of a language file of the account."
          },
          "language_file_name": {
            "readOnly": true,
            "description": "The name of that language file; a report that names none is printed with the English system file and that is the name answered here. Null when the report points at a language file of the account that no longer exists.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "substitution_variables": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The five substitution variables of the report, positional. An unused one is an empty string."
          },
          "widgets_show": {
            "$ref": "#/components/schemas/ReportWidgetsShow"
          },
          "snapshots_with_scheduled_executions": {
            "type": "boolean",
            "description": "Whether a snapshot of the report is stored every time it is delivered by a schedule."
          },
          "ai_brief": {
            "description": "What the AI summaries of this report know about the client, or null when the report carries no brief. It is the middle of the three levels a summary reads: account, report and widget.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "SavedFilterCondition": {
        "type": "object",
        "description": "One condition of a saved filter, in the same shape the filters block of a widget definition takes.",
        "required": [
          "field",
          "operator",
          "value",
          "type_of_field"
        ],
        "properties": {
          "field": {
            "type": "string",
            "description": "The filter field in the public vocabulary of the integration."
          },
          "operator": {
            "type": "string",
            "description": "The operator in the public vocabulary of the integration."
          },
          "value": {
            "type": "string",
            "description": "The value the condition compares against, always as text. Several values are separated by commas where the integration accepts a list."
          },
          "type_of_field": {
            "description": "dimension or metric, for the integrations that ask for it. Null everywhere else.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "additionalProperties": false
      },
      "SavedFilterConditions": {
        "type": "object",
        "description": "The conditions of a saved filter. The same shape as the filters block of a widget definition, so a filter read here can be sent as the filters of a widget.",
        "required": [
          "combination",
          "conditions"
        ],
        "properties": {
          "combination": {
            "type": "string",
            "enum": [
              "and",
              "or"
            ],
            "description": "How the conditions are joined. Most integrations fix it and only a few let the request choose."
          },
          "conditions": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/SavedFilterCondition"
            }
          }
        },
        "additionalProperties": false
      },
      "SavedFilter": {
        "type": "object",
        "description": "One saved filter of the account.",
        "required": [
          "filter_key",
          "name",
          "integration_id",
          "level",
          "is_system_defined",
          "conditions",
          "not_expressible_reason"
        ],
        "properties": {
          "filter_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "The filter. Published by a read and never written: a body that carries it is accepted, ignored and named in a READ_ONLY_FIELD_IGNORED warning, so the object a read answers can be sent back as it is. It is null in ONE place: the preview of a dry run of a creation, where the row does not exist yet and so has no key.",
            "readOnly": true
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "display_name": {
            "description": "The name shown on a widget that uses it, when it differs from the name.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "description": "A note for the account's own staff.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "integration_id": {
            "description": "The integration the filter belongs to, or null when the data source has no public id.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "integration": {
            "type": "string",
            "description": "The name the product gives a data source with no public integration_id. Present only in that case. Published by a read and never written: a body that carries it is accepted, ignored and named in a READ_ONLY_FIELD_IGNORED warning, so the object a read answers can be sent back as it is.",
            "readOnly": true
          },
          "level": {
            "description": "The data view the filter belongs to. Most filters belong to none and are read as null; when the one they do belong to has no public name, level is null and not_expressible_reason says level_not_available.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "is_system_defined": {
            "type": "boolean",
            "description": "Whether the filter is one the platform ships. Published by a read and never written: a body that carries it is accepted, ignored and named in a READ_ONLY_FIELD_IGNORED warning, so the object a read answers can be sent back as it is.",
            "readOnly": true
          },
          "conditions": {
            "description": "The conditions, or null when they cannot be named in public.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/SavedFilterConditions"
              },
              {
                "type": "null"
              }
            ]
          },
          "not_expressible_reason": {
            "description": "Why conditions is null: the data source has no public id, a field or an operator has no public name, the filter carries no condition, the data view it is read in has no public name, or the stored conditions could not be read. Null when the conditions are published. Published by a read and never written: a body that carries it is accepted, ignored and named in a READ_ONLY_FIELD_IGNORED warning, so the object a read answers can be sent back as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "source_not_available",
                  "not_in_catalog",
                  "no_conditions",
                  "level_not_available",
                  "unreadable"
                ]
              },
              {
                "type": "null"
              }
            ],
            "readOnly": true
          },
          "not_expressible_message": {
            "description": "The same thing in a sentence. Published by a read and never written: a body that carries it is accepted, ignored and named in a READ_ONLY_FIELD_IGNORED warning, so the object a read answers can be sent back as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "readOnly": true
          }
        }
      },
      "FiltersListRequest": {
        "type": "object",
        "description": "All of them, or only the ones of one integration.",
        "properties": {
          "integration_id": {
            "type": "string",
            "description": "Only the entries of this integration. From /v1/integrations. Omit it for all of them."
          }
        },
        "additionalProperties": true
      },
      "FiltersListData": {
        "type": "object",
        "description": "The saved filters of the account, ordered by name.",
        "required": [
          "filters",
          "count"
        ],
        "properties": {
          "filters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SavedFilter"
            }
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "FiltersListResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/FiltersListData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "FilterGetRequest": {
        "type": "object",
        "required": [
          "filter_key"
        ],
        "properties": {
          "filter_key": {
            "type": "string",
            "description": "The filter to read. From /v1/filters/list."
          }
        },
        "additionalProperties": true
      },
      "FilterGetData": {
        "type": "object",
        "description": "The filter.",
        "required": [
          "filter"
        ],
        "properties": {
          "filter": {
            "$ref": "#/components/schemas/SavedFilter"
          }
        }
      },
      "FilterGetResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/FilterGetData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "CalculatedMetricExpression": {
        "type": "object",
        "description": "A metric written as a formula.",
        "required": [
          "format",
          "formula",
          "green_when_increases"
        ],
        "properties": {
          "format": {
            "description": "How the result is formatted. time_millis and text are two older values outside the four the editor offers, published as they are stored.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "none",
                  "time",
                  "currency",
                  "percentage",
                  "time_millis",
                  "text"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "formula": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The formula as a flat list of infix tokens: the metric ids as the definition stores them, normally the ones /v1/fields publishes, plus the operators + - * /, parentheses and literal numbers. A metric the product retired can still appear in an older formula."
          },
          "green_when_increases": {
            "type": "boolean",
            "description": "Whether a rise of the metric is shown as a good thing."
          }
        }
      },
      "CalculatedMetricFunction": {
        "type": "object",
        "description": "A metric aggregated over a period.",
        "required": [
          "metric",
          "function",
          "time_period"
        ],
        "properties": {
          "metric": {
            "description": "The metric being aggregated, named as /v1/fields names it.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "function": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "average",
                  "min",
                  "max"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "time_period": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "day",
                  "week",
                  "month"
                ]
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "CalculatedMetricFiltered": {
        "type": "object",
        "description": "A metric read through a saved filter.",
        "required": [
          "metric",
          "filter_key"
        ],
        "properties": {
          "metric": {
            "description": "The metric being read, named as /v1/fields names it.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "filter_key": {
            "description": "The saved filter it is read through. From /v1/filters/list. It is published as it is stored and may name a filter that was deleted, which /v1/filters/get answers with 404.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "CalculatedMetric": {
        "type": "object",
        "description": "One calculated metric of the account. Only the block of its own kind is filled in, and all three are null when the definition cannot be named in public.",
        "required": [
          "calculated_metric_key",
          "name",
          "integration_id",
          "level",
          "type",
          "is_system_defined",
          "expression",
          "function",
          "filtered",
          "not_expressible_reason"
        ],
        "properties": {
          "calculated_metric_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "The metric. Published by a read and never written: a body that carries it is accepted, ignored and named in a READ_ONLY_FIELD_IGNORED warning, so the object a read answers can be sent back as it is. It is null in ONE place: the preview of a dry run of a creation, where the row does not exist yet and so has no key.",
            "readOnly": true
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "display_name": {
            "description": "The name shown on a widget that uses it, when it differs from the name.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "description": "A note for the account's own staff.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "integration_id": {
            "description": "The integration the metric belongs to, or null when the data source has no public id.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "integration": {
            "type": "string",
            "description": "The name the product gives a data source with no public integration_id. Present only in that case. Published by a read and never written: a body that carries it is accepted, ignored and named in a READ_ONLY_FIELD_IGNORED warning, so the object a read answers can be sent back as it is.",
            "readOnly": true
          },
          "level": {
            "description": "The data view the metric belongs to. Most metrics belong to none and are read as null; when the one they do belong to has no public name, level is null and not_expressible_reason says level_not_available.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "type": {
            "description": "Which of the three kinds it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "expression",
                  "function",
                  "filtered"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "is_system_defined": {
            "type": "boolean",
            "description": "Whether the metric is one the platform ships. Published by a read and never written: a body that carries it is accepted, ignored and named in a READ_ONLY_FIELD_IGNORED warning, so the object a read answers can be sent back as it is.",
            "readOnly": true
          },
          "expression": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/CalculatedMetricExpression"
              },
              {
                "type": "null"
              }
            ]
          },
          "function": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/CalculatedMetricFunction"
              },
              {
                "type": "null"
              }
            ]
          },
          "filtered": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/CalculatedMetricFiltered"
              },
              {
                "type": "null"
              }
            ]
          },
          "not_expressible_reason": {
            "description": "Why the definition is not published as it stands. With source_not_available and with unreadable the three blocks are null; with level_not_available only level is, because the id of a metric does not depend on the data view it is read in. Null when there is nothing to report. Published by a read and never written: a body that carries it is accepted, ignored and named in a READ_ONLY_FIELD_IGNORED warning, so the object a read answers can be sent back as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "source_not_available",
                  "level_not_available",
                  "unreadable"
                ]
              },
              {
                "type": "null"
              }
            ],
            "readOnly": true
          },
          "not_expressible_message": {
            "description": "The same thing in a sentence. Published by a read and never written: a body that carries it is accepted, ignored and named in a READ_ONLY_FIELD_IGNORED warning, so the object a read answers can be sent back as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "readOnly": true
          }
        }
      },
      "CalculatedMetricsListRequest": {
        "type": "object",
        "description": "All of them, or only the ones of one integration.",
        "properties": {
          "integration_id": {
            "type": "string",
            "description": "Only the entries of this integration. From /v1/integrations. Omit it for all of them."
          }
        },
        "additionalProperties": true
      },
      "CalculatedMetricsListData": {
        "type": "object",
        "description": "The calculated metrics of the account, ordered by name.",
        "required": [
          "calculated_metrics",
          "count"
        ],
        "properties": {
          "calculated_metrics": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CalculatedMetric"
            }
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "CalculatedMetricsListResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/CalculatedMetricsListData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "CalculatedMetricGetRequest": {
        "type": "object",
        "required": [
          "calculated_metric_key"
        ],
        "properties": {
          "calculated_metric_key": {
            "type": "string",
            "description": "The metric to read. From /v1/calculated-metrics/list."
          }
        },
        "additionalProperties": true
      },
      "CalculatedMetricGetData": {
        "type": "object",
        "description": "The metric.",
        "required": [
          "calculated_metric"
        ],
        "properties": {
          "calculated_metric": {
            "$ref": "#/components/schemas/CalculatedMetric"
          }
        }
      },
      "CalculatedMetricGetResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/CalculatedMetricGetData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "BrandTemplateSummary": {
        "type": "object",
        "description": "One brand template as a listing shows it.",
        "required": [
          "brand_template_key",
          "name",
          "is_system",
          "has_logo",
          "has_cover_image"
        ],
        "properties": {
          "brand_template_key": {
            "type": "string",
            "description": "The brand template. The empty string is the brand of the platform."
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "description": "A note for the account's own staff.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "is_system": {
            "type": "boolean",
            "description": "Whether this is the brand of the platform rather than one of the account."
          },
          "has_logo": {
            "type": "boolean",
            "description": "Whether the template carries a logo."
          },
          "has_cover_image": {
            "type": "boolean",
            "description": "Whether the template carries a cover image."
          },
          "primary_color": {
            "description": "The main colour of the brand, as #RRGGBB.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "font_family": {
            "description": "The font of the PDF.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "BrandTemplateImage": {
        "type": "object",
        "description": "An image the template carries. The file itself never travels in JSON and it is not read to be measured, so no size is published.",
        "required": [
          "present"
        ],
        "properties": {
          "present": {
            "type": "boolean",
            "description": "Always true. The whole object is null when there is no image."
          }
        }
      },
      "BrandTemplate": {
        "type": "object",
        "description": "Every setting of one brand template. The two images are reported as present or absent and never carried.",
        "required": [
          "brand_template_key",
          "is_system",
          "name",
          "colors",
          "logo",
          "cover_image"
        ],
        "properties": {
          "brand_template_key": {
            "type": "string",
            "description": "The brand template. The empty string is the brand of the platform."
          },
          "is_system": {
            "type": "boolean",
            "description": "Whether this is the brand of the platform rather than one of the account."
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "description": "A note for the account's own staff.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "font_family": {
            "description": "The font of the PDF.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "margin_top": {
            "description": "The top margin of the page, in points.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "margin_bottom": {
            "description": "The bottom margin of the page, in points.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "margin_left": {
            "description": "The left margin of the page, in points.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "margin_right": {
            "description": "The right margin of the page, in points.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_title_text_color": {
            "description": "The colour of the title of the cover, as #RRGGBB.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_title_font_size": {
            "description": "The size of the title of the cover.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_title_font_weight": {
            "description": "The weight of the title of the cover.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "normal",
                  "bold"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "report_subtitle_text_color": {
            "description": "The colour of the subtitle of the cover, as #RRGGBB.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_subtitle_font_size": {
            "description": "The size of the subtitle of the cover.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_subtitle_font_weight": {
            "description": "The weight of the subtitle of the cover.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "normal",
                  "bold"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "section_name_text_color": {
            "description": "The colour of the name of a section, as #RRGGBB.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "section_name_font_size": {
            "description": "The size of the name of a section.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "section_name_font_weight": {
            "description": "The weight of the name of a section.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "normal",
                  "bold"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "date_range_text_color": {
            "description": "The colour of the period line of a section, as #RRGGBB.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "date_range_font_size": {
            "description": "The size of the period line of a section.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "date_range_font_weight": {
            "description": "The weight of the period line of a section.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "normal",
                  "bold"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "component_name_text_color": {
            "description": "The colour of the name of a widget, as #RRGGBB.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "component_name_font_size": {
            "description": "The size of the name of a widget.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "component_name_font_weight": {
            "description": "The weight of the name of a widget.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "normal",
                  "bold"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "footer_text_color": {
            "description": "The colour of the footer, as #RRGGBB.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "footer_font_size": {
            "description": "The size of the footer.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "footer_font_weight": {
            "description": "The weight of the footer.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "normal",
                  "bold"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "include_logo": {
            "type": "boolean",
            "description": "Whether the report prints the logo."
          },
          "include_section_line": {
            "type": "boolean",
            "description": "Whether a line is drawn under the name of a section."
          },
          "section_line_color": {
            "description": "The colour of that line, as #RRGGBB.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "component_name_background_color": {
            "description": "The background behind the name of a widget, as #RRGGBB.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "component_content_text_color": {
            "description": "The colour of the content of a widget, as #RRGGBB.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "component_content_font_size": {
            "description": "The size of the content of a widget.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "colors": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The palette the charts are drawn with, in order, as #RRGGBB."
          },
          "footer_content": {
            "description": "The text of the footer.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "include_footer_line": {
            "type": "boolean",
            "description": "Whether a line is drawn above the footer."
          },
          "footer_line_color": {
            "description": "The colour of that line, as #RRGGBB.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "primary_color": {
            "description": "The main colour of the brand, as #RRGGBB.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "cover_style": {
            "type": "string",
            "enum": [
              "gradient",
              "plain",
              "aurora",
              "circles",
              "waves",
              "diagonal",
              "rings",
              "dots",
              "bubbles",
              "contours",
              "blocks",
              "image"
            ],
            "description": "The style the front cover is drawn with. A template that never picked one is read as plain, which is what it is drawn with."
          },
          "cover_tone": {
            "type": "string",
            "enum": [
              "auto",
              "light",
              "dark",
              "custom"
            ],
            "description": "The tone of the text of the cover. auto takes it from the primary colour and is what a template that never picked one is read as."
          },
          "cover_text_color": {
            "description": "The colour of the cover text when the tone is custom, as #RRGGBB.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "cover_inverted": {
            "type": "boolean",
            "description": "Whether the cover is drawn on a coloured surface instead of white paper."
          },
          "prepared_by": {
            "description": "The line naming who prepared the report.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "prepared_for": {
            "description": "The line naming who it was prepared for.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "toc_style": {
            "type": "string",
            "enum": [
              "cards",
              "rule",
              "band",
              "timeline"
            ],
            "description": "The style of the table of contents. A template that never picked one is read as timeline."
          },
          "portal_header_style": {
            "type": "string",
            "enum": [
              "none",
              "letterhead",
              "card",
              "band"
            ],
            "description": "The header the client portal wears. none draws no header and is what a template that never picked one is read as."
          },
          "portal_header_logo": {
            "type": "boolean",
            "description": "Whether that header carries the logo."
          },
          "web_font_family": {
            "type": "string",
            "description": "The font of the client portal and of a public link. app_default is the font of the application."
          },
          "logo": {
            "description": "The logo, or null when there is none.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/BrandTemplateImage"
              },
              {
                "type": "null"
              }
            ]
          },
          "cover_image": {
            "description": "The cover image, or null when there is none.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/BrandTemplateImage"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "BrandTemplatesListRequest": {
        "type": "object",
        "description": "No parameters.",
        "properties": {},
        "additionalProperties": true
      },
      "BrandTemplatesListData": {
        "type": "object",
        "description": "The brand of the platform first, then the templates of the account ordered by name.",
        "required": [
          "brand_templates",
          "count"
        ],
        "properties": {
          "brand_templates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BrandTemplateSummary"
            }
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "BrandTemplatesListResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/BrandTemplatesListData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "BrandTemplateGetRequest": {
        "type": "object",
        "required": [
          "brand_template_key"
        ],
        "properties": {
          "brand_template_key": {
            "type": "string",
            "description": "The brand template to read. From /v1/brand-templates/list; the empty string is the brand of the platform."
          }
        },
        "additionalProperties": true
      },
      "BrandTemplateGetData": {
        "type": "object",
        "description": "The brand template.",
        "required": [
          "brand_template"
        ],
        "properties": {
          "brand_template": {
            "$ref": "#/components/schemas/BrandTemplate"
          }
        }
      },
      "BrandTemplateGetResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/BrandTemplateGetData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "LanguageFile": {
        "type": "object",
        "description": "One language file a report can be printed with. The literals are not published.",
        "required": [
          "language_file_key",
          "name",
          "language",
          "is_system"
        ],
        "properties": {
          "language_file_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "The language file. It is null in ONE place: the preview of a dry run of a creation, where the row does not exist yet and so has no key."
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "language": {
            "description": "The ISO 639-1 code of the literals. Null on the files of the account made before the language was asked for.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "is_system": {
            "type": "boolean",
            "description": "Whether the file is one of the sixteen the platform ships."
          },
          "file_name": {
            "description": "The name of the .txt the file was uploaded from. Null for the files of the platform.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "description": "A note for the account's own staff.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "LanguageFilesListRequest": {
        "type": "object",
        "description": "No parameters.",
        "properties": {},
        "additionalProperties": true
      },
      "LanguageFilesListData": {
        "type": "object",
        "description": "The sixteen files of the platform first, then the ones of the account ordered by name.",
        "required": [
          "language_files",
          "count"
        ],
        "properties": {
          "language_files": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LanguageFile"
            }
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "LanguageFilesListResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/LanguageFilesListData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "EmailTemplate": {
        "type": "object",
        "description": "One email template of the account. The body is not published.",
        "required": [
          "email_template_key",
          "name",
          "format",
          "subject"
        ],
        "properties": {
          "email_template_key": {
            "type": "string",
            "description": "The email template."
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "description": "A note for the account's own staff.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "format": {
            "description": "Whether the body is HTML or plain text.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "plain_text",
                  "html"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "sender": {
            "description": "The name the email is sent under.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "sender_email": {
            "description": "The address the email is sent from.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "reply_to_email": {
            "description": "The address a reply goes to.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "subject": {
            "description": "The subject of the email.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "EmailTemplatesListRequest": {
        "type": "object",
        "description": "No parameters.",
        "properties": {},
        "additionalProperties": true
      },
      "EmailTemplatesListData": {
        "type": "object",
        "description": "The email templates of the account, ordered by name.",
        "required": [
          "email_templates",
          "count"
        ],
        "properties": {
          "email_templates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EmailTemplate"
            }
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "EmailTemplatesListResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/EmailTemplatesListData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "ReportsTakeSnapshotRequest": {
        "type": "object",
        "required": [
          "report_key"
        ],
        "additionalProperties": false,
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report to file a snapshot of. Every kind of report is accepted, templates included."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          },
          "confirmation_token": {
            "type": "string",
            "description": "The token the first call answered with, to apply the operation."
          }
        }
      },
      "ReportsTakeSnapshotData": {
        "type": "object",
        "description": "The request that was queued. The file itself appears in /v1/snapshots/list minutes later.",
        "required": [
          "report_key",
          "status",
          "ai_summaries",
          "format"
        ],
        "properties": {
          "schedule_key": {
            "description": "The one-off delivery that will produce the snapshot. Stop it with /v1/schedules/delete while it has not run.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_key": {
            "type": "string"
          },
          "report_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "queued"
            ],
            "description": "The request is queued; the platform takes the snapshot on its next pass."
          },
          "ai_summaries": {
            "type": "integer",
            "description": "AI summary widgets the report carries, every one of which is written again when the snapshot is taken."
          },
          "format": {
            "type": "string",
            "enum": [
              "pdf"
            ]
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "ReportsTakeSnapshotResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/ReportsTakeSnapshotData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "Snapshot": {
        "type": "object",
        "description": "One snapshot of a report. The file itself is downloaded, never published inline, and its size is not published at all.",
        "required": [
          "snapshot_key",
          "report_key",
          "type",
          "status"
        ],
        "properties": {
          "snapshot_key": {
            "type": "string"
          },
          "report_key": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "manual",
              "scheduled"
            ],
            "description": "manual was asked for with /v1/reports/take-snapshot or from the application; scheduled was filed by a scheduled delivery."
          },
          "status": {
            "type": "string",
            "enum": [
              "success",
              "error"
            ],
            "description": "A snapshot of an execution that failed carries no file."
          },
          "filename": {
            "description": "The name the file is downloaded under.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "created_at": {
            "description": "When the snapshot was filed, ISO 8601 UTC.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "download_url": {
            "description": "A signed link to the file, good for 900 seconds and for this snapshot alone. It needs no Authorization header, and it is the whole URL, ready to open. Null when the snapshot has no file, and null as well in the rare case of a node that does not know the public base of this API, where the file is still reachable by `snapshot_key` with the Bearer of the account.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "download_url_expires_at": {
            "description": "When that link stops working, ISO 8601 UTC. List again for a fresh one.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "SnapshotsListRequest": {
        "type": "object",
        "required": [
          "report_key"
        ],
        "additionalProperties": true,
        "properties": {
          "report_key": {
            "type": "string",
            "description": "The report."
          }
        }
      },
      "SnapshotsListData": {
        "type": "object",
        "required": [
          "report_key",
          "snapshots",
          "count"
        ],
        "properties": {
          "report_key": {
            "type": "string"
          },
          "snapshots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Snapshot"
            },
            "description": "Newest first. A report keeps its 13 newest snapshots."
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "SnapshotsListResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/SnapshotsListData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "SnapshotsDeleteRequest": {
        "type": "object",
        "required": [
          "snapshot_key"
        ],
        "additionalProperties": false,
        "properties": {
          "snapshot_key": {
            "type": "string",
            "description": "The snapshot to delete, from /v1/snapshots/list."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          },
          "confirmation_token": {
            "type": "string",
            "description": "The token the first call answered with, to apply the operation."
          }
        }
      },
      "SnapshotsDeleteData": {
        "type": "object",
        "required": [
          "snapshot_key"
        ],
        "properties": {
          "snapshot_key": {
            "type": "string"
          },
          "report_key": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "deleted": {
            "type": "boolean",
            "description": "Present on the write. The preview carries permanent instead."
          },
          "permanent": {
            "type": "boolean",
            "description": "Present on the preview: the deletion has no inverse."
          },
          "before": {
            "type": "object",
            "description": "What the snapshot was, which is all a deletion can give back.",
            "properties": {
              "type": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "status": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "filename": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "created_at": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "SnapshotsDeleteResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/SnapshotsDeleteData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "FiltersCreateRequest": {
        "type": "object",
        "description": "A new saved filter. The body is the object /v1/filters/get answers. The keys a read publishes and a write has no column for -- filter_key, integration, is_system_defined, not_expressible_reason and not_expressible_message -- are declared here as readOnly: they are accepted, ignored and named one by one in a READ_ONLY_FIELD_IGNORED warning, so what a read answered can be sent straight back with nothing taken out of it. A null means \"leave it as it is\".",
        "required": [
          "name",
          "integration_id",
          "conditions"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 50,
            "description": "How the definition is listed in the account. 2 to 50 characters, with no character above the Basic Multilingual Plane."
          },
          "display_name": {
            "minLength": 2,
            "maxLength": 50,
            "description": "What is drawn on a widget that uses it. Left out or null on a creation, it is the name.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "maxLength": 250,
            "description": "A note for the account's own staff, at most 250 characters. An empty string takes it away.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "integration_id": {
            "type": "string",
            "description": "The integration the filter belongs to. It cannot be changed once the filter exists."
          },
          "level": {
            "description": "The data view the filter is read in. Required for the integrations capabilities lists under filters.level_required_for, and refused for every other one, whether or not it publishes data views for its widgets. The values it takes are the data views /v1/integrations/detail publishes for the integration; the application offers more of them, and a filter placed in one of those cannot be named here. It cannot be changed once the filter exists.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "conditions": {
            "$ref": "#/components/schemas/SavedFilterConditions",
            "description": "The whole set of conditions, which replaces whatever the filter had. The value of a condition cannot contain the sequence \"#%#\": it is what the parts of a stored condition are joined with, and a value carrying it would be read back cut in two. Every condition carries type_of_field, and its value is null unless the integration requires it: /v1/integrations/detail says which ones do. It is the same rule the filters block of a widget definition follows -- the key is required and the value is nullable -- so a filter read here is a filters block a widget takes. Where the filters of the integration belong to a data view, the field of every condition has to be one that view offers: a filter of the campaign resource of Google Ads does not take a condition on a field of ad_group, and one that does is refused with FILTER_FIELD_NOT_IN_DATA_VIEW."
          },
          "filter_key": {
            "type": "string",
            "readOnly": true,
            "description": "The key the row already has. Published by /v1/filters/get and written by nothing. It is accepted here so the object a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "integration": {
            "type": "string",
            "readOnly": true,
            "description": "The name of a data source this API publishes no integration_id for, which a read answers instead of integration_id. Published by /v1/filters/get and written by nothing. It is accepted here so the object a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "is_system_defined": {
            "type": "boolean",
            "readOnly": true,
            "description": "Published by /v1/filters/get and written by nothing. It is accepted here so the object a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "not_expressible_reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "readOnly": true,
            "description": "Published by /v1/filters/get and written by nothing. It is accepted here so the object a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "not_expressible_message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "readOnly": true,
            "description": "Published by /v1/filters/get and written by nothing. It is accepted here so the object a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          }
        }
      },
      "FiltersUpdateRequest": {
        "type": "object",
        "description": "Which filter to change and what of it. Everything the body does not carry is left as it is; conditions replaces the whole set. integration_id and level are accepted only when they name what the filter already is, so a read can be replayed, and refused otherwise. The keys a read publishes and a write has no column for -- integration, is_system_defined, not_expressible_reason and not_expressible_message -- are declared here as readOnly: they are accepted, ignored and named one by one in a READ_ONLY_FIELD_IGNORED warning, so what a read answered can be sent straight back with nothing taken out of it. A null means \"leave it as it is\".",
        "required": [
          "filter_key"
        ],
        "additionalProperties": false,
        "properties": {
          "filter_key": {
            "type": "string",
            "description": "The filter to change. From /v1/filters/list."
          },
          "name": {
            "minLength": 2,
            "maxLength": 50,
            "description": "How the definition is listed in the account. 2 to 50 characters, with no character above the Basic Multilingual Plane.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "display_name": {
            "minLength": 2,
            "maxLength": 50,
            "description": "What is drawn on a widget that uses it. Null or left out leaves it as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "maxLength": 250,
            "description": "A note for the account's own staff, at most 250 characters. An empty string takes it away.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "integration_id": {
            "description": "Only the integration the filter already has.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "level": {
            "description": "Only the data view the filter already has.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "conditions": {
            "$ref": "#/components/schemas/SavedFilterConditions",
            "description": "The whole set of conditions, which replaces whatever the filter had. The value of a condition cannot contain the sequence \"#%#\": it is what the parts of a stored condition are joined with, and a value carrying it would be read back cut in two. Every condition carries type_of_field, and its value is null unless the integration requires it: /v1/integrations/detail says which ones do. It is the same rule the filters block of a widget definition follows -- the key is required and the value is nullable -- so a filter read here is a filters block a widget takes. Where the filters of the integration belong to a data view, the field of every condition has to be one that view offers: a filter of the campaign resource of Google Ads does not take a condition on a field of ad_group, and one that does is refused with FILTER_FIELD_NOT_IN_DATA_VIEW."
          },
          "integration": {
            "type": "string",
            "readOnly": true,
            "description": "The name of a data source this API publishes no integration_id for, which a read answers instead of integration_id. Published by /v1/filters/get and written by nothing. It is accepted here so the object a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "is_system_defined": {
            "type": "boolean",
            "readOnly": true,
            "description": "Published by /v1/filters/get and written by nothing. It is accepted here so the object a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "not_expressible_reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "readOnly": true,
            "description": "Published by /v1/filters/get and written by nothing. It is accepted here so the object a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "not_expressible_message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "readOnly": true,
            "description": "Published by /v1/filters/get and written by nothing. It is accepted here so the object a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          }
        }
      },
      "FiltersDeleteRequest": {
        "type": "object",
        "required": [
          "filter_key"
        ],
        "additionalProperties": false,
        "properties": {
          "filter_key": {
            "type": "string",
            "description": "The filter to delete. From /v1/filters/list."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          },
          "confirmation_token": {
            "type": "string",
            "description": "The token the first call answered with, to apply the operation."
          }
        }
      },
      "FiltersCreateData": {
        "type": "object",
        "description": "The filter that was written, read back in the shape /v1/filters/get publishes.",
        "required": [
          "filter",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "filter": {
            "$ref": "#/components/schemas/SavedFilter"
          },
          "created_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The key of the filter, as every creation of this API publishes it."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "FiltersUpdateData": {
        "type": "object",
        "description": "The filter as it is now, and what it was. Sending `before` back through this endpoint puts the definition as it was -- and empties the cached results of the account again, because the cache is not part of what before restores.",
        "required": [
          "filter",
          "before",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "filter": {
            "$ref": "#/components/schemas/SavedFilter"
          },
          "before": {
            "$ref": "#/components/schemas/SavedFilter"
          },
          "dropped_account_cache": {
            "type": "boolean",
            "description": "Present on the write and true: updating a filter empties every cached result of the account, reports and Data Studio alike, once the new definition has been stored. A dry run does not carry it, because a dry run empties nothing; the DROPS_ACCOUNT_CACHE warning says what a real call would do."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "FiltersDeleteData": {
        "type": "object",
        "description": "What the filter was, which is all a deletion can give back.",
        "required": [
          "filter_key",
          "before",
          "dropped_account_cache",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "filter_key": {
            "type": "string"
          },
          "before": {
            "$ref": "#/components/schemas/SavedFilter"
          },
          "dropped_account_cache": {
            "type": "boolean",
            "description": "Always false: a deletion empties nothing."
          },
          "deleted": {
            "type": "boolean",
            "description": "Present on the write. The preview carries permanent instead."
          },
          "permanent": {
            "type": "boolean",
            "description": "Present on the preview: the deletion has no inverse."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "CalculatedMetricsCreateRequest": {
        "type": "object",
        "description": "A new calculated metric. The body is the object /v1/calculated-metrics/get answers -- the kind in type and its own block filled in, the other two null. The keys a read publishes and a write has no column for -- calculated_metric_key, integration, is_system_defined, not_expressible_reason and not_expressible_message -- are declared here as readOnly: they are accepted, ignored and named one by one in a READ_ONLY_FIELD_IGNORED warning, so what a read answered can be sent straight back with nothing taken out of it. A null means \"leave it as it is\".",
        "required": [
          "name",
          "integration_id",
          "type"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 50,
            "description": "How the definition is listed in the account. 2 to 50 characters, with no character above the Basic Multilingual Plane."
          },
          "display_name": {
            "minLength": 2,
            "maxLength": 50,
            "description": "What a widget draws as the column header. Left out or null on a creation, it is the name.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "maxLength": 250,
            "description": "A note for the account's own staff, at most 250 characters. An empty string takes it away.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "integration_id": {
            "type": "string",
            "description": "The integration the metric belongs to. It cannot be changed once the metric exists."
          },
          "level": {
            "description": "The data view the metric is read in. Required for the integrations capabilities lists under calculated_metrics.level_required_for, written by the platform for the ones under calculated_metrics.level_fixed_for -- which accept the value a read publishes for them and nothing else -- and refused for the rest. The values it takes are the data views /v1/integrations/detail publishes for the integration. It cannot be changed once the metric exists.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "expression",
              "function",
              "filtered"
            ],
            "description": "Which of the three kinds it is. It cannot be changed once the metric exists: each kind is stored in columns of its own."
          },
          "expression": {
            "anyOf": [
              {
                "type": "object",
                "description": "Kind expression: the formula and how its result is printed.",
                "additionalProperties": false,
                "properties": {
                  "format": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "none",
                      "time",
                      "currency",
                      "percentage",
                      null
                    ],
                    "description": "Required on a creation. The two older values a read may publish, time_millis and text, are kept by an update that does not name format and cannot be written."
                  },
                  "formula": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string"
                    },
                    "description": "A flat list of infix tokens: metric ids of /v1/fields for this integration, the operators + - * /, '(' and ')', and literal numbers. At least one metric, the parentheses balance, and an id the field catalogue does not publish is refused -- which is stricter than the application, where a token is a metric as soon as it starts with the prefix of an integration."
                  },
                  "green_when_increases": {
                    "type": "boolean",
                    "description": "Whether a rise of the metric is drawn as a good thing. True on a creation that does not name it."
                  }
                }
              },
              {
                "type": "null"
              }
            ],
            "description": "The block of the expression kind. Null, or left out, in a metric of another kind -- which is what a read publishes for it."
          },
          "function": {
            "anyOf": [
              {
                "type": "object",
                "description": "Kind function: one metric aggregated over a period.",
                "additionalProperties": false,
                "properties": {
                  "metric": {
                    "description": "A metric id of /v1/fields for this integration.",
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "function": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "average",
                      "min",
                      "max",
                      null
                    ]
                  },
                  "time_period": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "day",
                      "week",
                      "month",
                      null
                    ]
                  }
                }
              },
              {
                "type": "null"
              }
            ],
            "description": "The block of the function kind. Null, or left out, in a metric of another kind."
          },
          "filtered": {
            "anyOf": [
              {
                "type": "object",
                "description": "Kind filtered: one metric read through a saved filter of the same integration.",
                "additionalProperties": false,
                "properties": {
                  "metric": {
                    "description": "A metric id of /v1/fields for this integration.",
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "filter_key": {
                    "description": "A saved filter of the account and of this integration. From /v1/filters/list.",
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                }
              },
              {
                "type": "null"
              }
            ],
            "description": "The block of the filtered kind. Null, or left out, in a metric of another kind."
          },
          "calculated_metric_key": {
            "type": "string",
            "readOnly": true,
            "description": "The key the row already has. Published by /v1/calculated-metrics/get and written by nothing. It is accepted here so the object a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "integration": {
            "type": "string",
            "readOnly": true,
            "description": "The name of a data source this API publishes no integration_id for, which a read answers instead of integration_id. Published by /v1/calculated-metrics/get and written by nothing. It is accepted here so the object a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "is_system_defined": {
            "type": "boolean",
            "readOnly": true,
            "description": "Published by /v1/calculated-metrics/get and written by nothing. It is accepted here so the object a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "not_expressible_reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "readOnly": true,
            "description": "Published by /v1/calculated-metrics/get and written by nothing. It is accepted here so the object a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "not_expressible_message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "readOnly": true,
            "description": "Published by /v1/calculated-metrics/get and written by nothing. It is accepted here so the object a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          }
        }
      },
      "CalculatedMetricsUpdateRequest": {
        "type": "object",
        "description": "Which calculated metric to change and what of it. Everything the body does not carry is left as it is, inside the block of its kind too. integration_id, type and level are accepted only when they name what the metric already is. The keys a read publishes and a write has no column for -- integration, is_system_defined, not_expressible_reason and not_expressible_message -- are declared here as readOnly: they are accepted, ignored and named one by one in a READ_ONLY_FIELD_IGNORED warning, so what a read answered can be sent straight back with nothing taken out of it. A null means \"leave it as it is\".",
        "required": [
          "calculated_metric_key"
        ],
        "additionalProperties": false,
        "properties": {
          "calculated_metric_key": {
            "type": "string",
            "description": "The metric to change. From /v1/calculated-metrics/list."
          },
          "name": {
            "minLength": 2,
            "maxLength": 50,
            "description": "How the definition is listed in the account. 2 to 50 characters, with no character above the Basic Multilingual Plane.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "display_name": {
            "minLength": 2,
            "maxLength": 50,
            "description": "What is drawn on a widget that uses it. Null or left out leaves it as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "maxLength": 250,
            "description": "A note for the account's own staff, at most 250 characters. An empty string takes it away.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "integration_id": {
            "description": "Only the integration the metric already has.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "level": {
            "description": "Only the data view the metric already has.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "expression",
              "function",
              "filtered"
            ],
            "description": "Only the kind the metric already is."
          },
          "expression": {
            "anyOf": [
              {
                "type": "object",
                "description": "Kind expression: the formula and how its result is printed.",
                "additionalProperties": false,
                "properties": {
                  "format": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "none",
                      "time",
                      "currency",
                      "percentage",
                      null
                    ],
                    "description": "Required on a creation. The two older values a read may publish, time_millis and text, are kept by an update that does not name format and cannot be written."
                  },
                  "formula": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string"
                    },
                    "description": "A flat list of infix tokens: metric ids of /v1/fields for this integration, the operators + - * /, '(' and ')', and literal numbers. At least one metric, the parentheses balance, and an id the field catalogue does not publish is refused -- which is stricter than the application, where a token is a metric as soon as it starts with the prefix of an integration."
                  },
                  "green_when_increases": {
                    "type": "boolean",
                    "description": "Whether a rise of the metric is drawn as a good thing. True on a creation that does not name it."
                  }
                }
              },
              {
                "type": "null"
              }
            ],
            "description": "The block of the expression kind. Null, or left out, in a metric of another kind -- which is what a read publishes for it."
          },
          "function": {
            "anyOf": [
              {
                "type": "object",
                "description": "Kind function: one metric aggregated over a period.",
                "additionalProperties": false,
                "properties": {
                  "metric": {
                    "description": "A metric id of /v1/fields for this integration.",
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "function": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "average",
                      "min",
                      "max",
                      null
                    ]
                  },
                  "time_period": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "day",
                      "week",
                      "month",
                      null
                    ]
                  }
                }
              },
              {
                "type": "null"
              }
            ],
            "description": "The block of the function kind. Null, or left out, in a metric of another kind."
          },
          "filtered": {
            "anyOf": [
              {
                "type": "object",
                "description": "Kind filtered: one metric read through a saved filter of the same integration.",
                "additionalProperties": false,
                "properties": {
                  "metric": {
                    "description": "A metric id of /v1/fields for this integration.",
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "filter_key": {
                    "description": "A saved filter of the account and of this integration. From /v1/filters/list.",
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                }
              },
              {
                "type": "null"
              }
            ],
            "description": "The block of the filtered kind. Null, or left out, in a metric of another kind."
          },
          "integration": {
            "type": "string",
            "readOnly": true,
            "description": "The name of a data source this API publishes no integration_id for, which a read answers instead of integration_id. Published by /v1/calculated-metrics/get and written by nothing. It is accepted here so the object a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "is_system_defined": {
            "type": "boolean",
            "readOnly": true,
            "description": "Published by /v1/calculated-metrics/get and written by nothing. It is accepted here so the object a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "not_expressible_reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "readOnly": true,
            "description": "Published by /v1/calculated-metrics/get and written by nothing. It is accepted here so the object a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "not_expressible_message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "readOnly": true,
            "description": "Published by /v1/calculated-metrics/get and written by nothing. It is accepted here so the object a read answers can be sent straight back as a body; the key is ignored and the answer carries a READ_ONLY_FIELD_IGNORED warning naming it."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          }
        }
      },
      "CalculatedMetricsDeleteRequest": {
        "type": "object",
        "required": [
          "calculated_metric_key"
        ],
        "additionalProperties": false,
        "properties": {
          "calculated_metric_key": {
            "type": "string",
            "description": "The metric to delete. From /v1/calculated-metrics/list."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          },
          "confirmation_token": {
            "type": "string",
            "description": "The token the first call answered with, to apply the operation."
          }
        }
      },
      "CalculatedMetricsCreateData": {
        "type": "object",
        "description": "The metric that was written, read back in the shape /v1/calculated-metrics/get publishes. /v1/fields lists it from now on.",
        "required": [
          "calculated_metric",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "calculated_metric": {
            "$ref": "#/components/schemas/CalculatedMetric"
          },
          "created_keys": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "CalculatedMetricsUpdateData": {
        "type": "object",
        "description": "The metric as it is now, and what it was. Sending `before` back through this endpoint puts the definition as it was, and empties the cached results of the account again.",
        "required": [
          "calculated_metric",
          "before",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "calculated_metric": {
            "$ref": "#/components/schemas/CalculatedMetric"
          },
          "before": {
            "$ref": "#/components/schemas/CalculatedMetric"
          },
          "dropped_account_cache": {
            "type": "boolean",
            "description": "Present on the write and true: updating a calculated metric empties every cached result of the account, reports and Data Studio alike, once the new definition has been stored. A dry run does not carry it, because a dry run empties nothing; the DROPS_ACCOUNT_CACHE warning says what a real call would do."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "CalculatedMetricsDeleteData": {
        "type": "object",
        "description": "What the metric was, which is all a deletion can give back.",
        "required": [
          "calculated_metric_key",
          "before",
          "dropped_account_cache",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "calculated_metric_key": {
            "type": "string"
          },
          "before": {
            "$ref": "#/components/schemas/CalculatedMetric"
          },
          "dropped_account_cache": {
            "type": "boolean",
            "description": "Always false: a deletion empties nothing."
          },
          "deleted": {
            "type": "boolean"
          },
          "permanent": {
            "type": "boolean"
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "FiltersCreateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/FiltersCreateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "FiltersUpdateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/FiltersUpdateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "FiltersDeleteResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/FiltersDeleteData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "CalculatedMetricsCreateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/CalculatedMetricsCreateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "CalculatedMetricsUpdateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/CalculatedMetricsUpdateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "CalculatedMetricsDeleteResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/CalculatedMetricsDeleteData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "EntityUsageReport": {
        "type": "object",
        "description": "One report that reads the definition, with how much of it does.",
        "required": [
          "report_key",
          "internal_name",
          "in_trash",
          "widgets",
          "sections"
        ],
        "additionalProperties": false,
        "properties": {
          "report_key": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The report the widgets and sections belong to. It can be the key of a TEMPLATE, because a widget of a template based report belongs to the template; and it is null when the report of the element could not be resolved, in which case the entry stands for a single widget or section."
          },
          "internal_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "What the report is called inside the account. Null when the key names a template or a report that is not there any more."
          },
          "in_trash": {
            "type": "boolean",
            "description": "Whether the report is in the trash. A report in the trash still reads the definition."
          },
          "widgets": {
            "type": "integer",
            "description": "How many widgets of it read the definition."
          },
          "sections": {
            "type": "integer",
            "description": "How many sections of it read the definition as a global filter."
          }
        }
      },
      "EntityUsageEntity": {
        "type": "object",
        "description": "One definition of the account that reads the one a deletion was asked of.",
        "additionalProperties": true,
        "properties": {
          "calculated_metric_key": {
            "type": "string",
            "description": "Present in calculated_metrics."
          },
          "portal_view_key": {
            "type": "string",
            "description": "Present in portal_views."
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "What it is called."
          }
        }
      },
      "EntityUsage": {
        "type": "object",
        "description": "What still reads a saved filter or a calculated metric, published inside details.used_by of an IN_USE. Only the families the question covers are present: a filter is asked about reports, calculated metrics, portal views, alerts and Data Studio account groups, and carries the five; a calculated metric is asked about reports and account groups, and carries those two alone rather than empty lists nobody looked into.",
        "required": [
          "reports",
          "account_groups"
        ],
        "additionalProperties": false,
        "properties": {
          "reports": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EntityUsageReport"
            }
          },
          "calculated_metrics": {
            "type": "array",
            "description": "Only for a saved filter: the filtered metrics built on it.",
            "items": {
              "$ref": "#/components/schemas/EntityUsageEntity"
            }
          },
          "portal_views": {
            "type": "array",
            "description": "Only for a saved filter: the portal views that hand it to their clients.",
            "items": {
              "$ref": "#/components/schemas/EntityUsageEntity"
            }
          },
          "alerts": {
            "type": "integer",
            "description": "Only for a saved filter: how many alerts of the account are evaluated with it."
          },
          "account_groups": {
            "type": "integer",
            "description": "How many Data Studio account groups read it."
          }
        }
      },
      "EmailTemplateWithBody": {
        "type": "object",
        "description": "One email template of the account WITH its body, which is what /v1/email-templates/get answers and what the two writes take back. The body is left out of the object when the row carries none.",
        "required": [
          "email_template_key",
          "name",
          "format",
          "subject"
        ],
        "properties": {
          "email_template_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "The email template. It is null in ONE place: the preview of a dry run of a creation, where the row does not exist yet and so has no key."
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "description": "A note for the account's own staff.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "format": {
            "description": "Whether the body is HTML or plain text.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "plain_text",
                  "html"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "sender": {
            "description": "The name the email is sent under.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "sender_email": {
            "description": "The address the email is sent from.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "reply_to_email": {
            "description": "The address a reply goes to.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "subject": {
            "description": "The subject of the email.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "body": {
            "type": "string",
            "description": "The body of the email, HTML or plain text according to format. Stored exactly as it was sent: it is not sanitized, because a template is a whole HTML document and the application never paints it."
          }
        }
      },
      "LanguageFileDetail": {
        "type": "object",
        "description": "One language file with the signed link its .txt is downloaded from. The literals never travel in JSON.",
        "required": [
          "language_file_key",
          "name",
          "language",
          "is_system",
          "download_url",
          "download_url_expires_at"
        ],
        "properties": {
          "language_file_key": {
            "type": "string",
            "description": "The language file."
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "language": {
            "description": "The ISO 639-1 code of the literals. Null on the files of the account made before the language was asked for.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "is_system": {
            "type": "boolean",
            "description": "Whether the file is one of the sixteen the platform ships."
          },
          "file_name": {
            "description": "The name of the .txt the file was uploaded from. Null for the files of the platform.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "description": "A note for the account's own staff.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "download_url": {
            "description": "A signed link to GET /v1/language-files/download for THIS file. It needs no Authorization header, is good for one file and expires 900 seconds after this call. Null on a node that does not publish a base URL for the API.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "download_url_expires_at": {
            "description": "When download_url stops working (ISO 8601).",
            "format": "date-time",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "LanguageFileUpload": {
        "type": "object",
        "description": "The .txt of the literals, in the format the upload of the application takes: UTF-8, one `key = value` per line, # for a comment, an apostrophe written as &#39;. A line with two equals signs, a key that is there twice or a backslash is refused with the message of the parser, naming the line. Keys no field catalogue of the platform knows are DROPPED and counted in ignored_keys. A line that begins with no key at all is refused the same way. A file that leaves NOT ONE literal -- empty, all comments, or all keys nobody knows -- is refused: on an update the file REPLACES every literal, so an empty one would take the whole file away. A key with no value is dropped and is NOT counted in ignored_keys, which is how the application says 'do not translate this one'. And a value carrying the sequence `_@#@_` is refused, because that is what the literals of a file are kept apart with and such a value would be read back cut in half.",
        "required": [
          "name",
          "content_base64"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 50,
            "description": "The name of the .txt, ending in .txt. It is what the application shows beside the file."
          },
          "content_base64": {
            "type": "string",
            "description": "The file itself, base64 encoded, at most 716800 decoded bytes. Not a data URI."
          }
        }
      },
      "EntityUsageSchedule": {
        "type": "object",
        "description": "One scheduled delivery sent with the email template.",
        "required": [
          "schedule_key",
          "report_key",
          "frequency"
        ],
        "additionalProperties": false,
        "properties": {
          "schedule_key": {
            "type": "string",
            "description": "The delivery, as /v1/schedules/list names it."
          },
          "report_key": {
            "description": "The report it delivers.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "frequency": {
            "description": "How often it goes out.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "EntityUsageSchedules": {
        "type": "object",
        "description": "What still sends an email template, published inside details.used_by of an IN_USE.",
        "required": [
          "schedules",
          "client_portal"
        ],
        "additionalProperties": false,
        "properties": {
          "schedules": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EntityUsageSchedule"
            }
          },
          "client_portal": {
            "type": "boolean",
            "description": "Whether the client portal of the account sends its notifications with this template, which is chosen in the application."
          }
        }
      },
      "EntityUsageReportRef": {
        "type": "object",
        "description": "One report printed with the language file.",
        "required": [
          "report_key",
          "internal_name",
          "in_trash"
        ],
        "additionalProperties": false,
        "properties": {
          "report_key": {
            "type": "string"
          },
          "internal_name": {
            "description": "What the report is called inside the account.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "in_trash": {
            "type": "boolean",
            "description": "Whether the report is in the trash. A report in the trash still points at the file and would come back pointing at nothing."
          }
        }
      },
      "EntityUsageReports": {
        "type": "object",
        "description": "What still reads a language file, published inside details.used_by of an IN_USE.",
        "required": [
          "reports"
        ],
        "additionalProperties": false,
        "properties": {
          "reports": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EntityUsageReportRef"
            }
          }
        }
      },
      "EmailTemplatesGetRequest": {
        "type": "object",
        "required": [
          "email_template_key"
        ],
        "additionalProperties": false,
        "properties": {
          "email_template_key": {
            "type": "string",
            "description": "The template. From /v1/email-templates/list."
          }
        }
      },
      "EmailTemplatesGetData": {
        "type": "object",
        "required": [
          "email_template"
        ],
        "properties": {
          "email_template": {
            "$ref": "#/components/schemas/EmailTemplateWithBody"
          }
        }
      },
      "EmailTemplatesGetResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/EmailTemplatesGetData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "EmailTemplatesCreateRequest": {
        "type": "object",
        "description": "A new email template. The eight fields are the ones the editor of the application shows.",
        "required": [
          "name",
          "format",
          "sender",
          "sender_email",
          "reply_to_email",
          "subject",
          "body"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 50,
            "description": "How the template is listed in the account. 2 to 50 characters, with no character above the Basic Multilingual Plane."
          },
          "internal_notes": {
            "description": "A note for the account's own staff, at most 250 characters. An empty string takes it away.",
            "maxLength": 250,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "format": {
            "type": "string",
            "enum": [
              "plain_text",
              "html"
            ],
            "description": "Whether the body is HTML or plain text."
          },
          "sender": {
            "type": "string",
            "minLength": 5,
            "maxLength": 70,
            "description": "The name the email is sent under."
          },
          "sender_email": {
            "type": "string",
            "description": "The From: address. It has to be the address of a user of this account or share the domain of one, which the application does not ask for: this API sends mail on behalf of the account."
          },
          "reply_to_email": {
            "type": "string",
            "description": "Where a reply goes. The same rule as sender_email."
          },
          "subject": {
            "type": "string",
            "minLength": 5,
            "maxLength": 150,
            "description": "The subject of the email. It can carry the substitution variables of a delivery; %%AI_SUMMARY%% is blanked in a subject."
          },
          "body": {
            "type": "string",
            "minLength": 5,
            "maxLength": 75000,
            "description": "The body of the email, HTML or plain text according to format. It is stored exactly as it is sent -- it is NOT sanitized -- and it can carry the substitution variables of a delivery. The Basic Multilingual Plane is not asked of it: the body may carry any character, emoji included."
          },
          "email_template_key": {
            "description": "Accepted and ignored: it is part of what a read publishes and a creation has no column for, so the object /v1/email-templates/get answers can be sent straight back. It comes back as a READ_ONLY_FIELD_IGNORED warning.",
            "readOnly": true,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          }
        }
      },
      "EmailTemplatesCreateData": {
        "type": "object",
        "description": "The template that was written, in the shape /v1/email-templates/get publishes.",
        "required": [
          "email_template",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "email_template": {
            "$ref": "#/components/schemas/EmailTemplateWithBody"
          },
          "created_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The key of the template, as every creation of this API publishes it."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "EmailTemplatesCreateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/EmailTemplatesCreateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "EmailTemplatesUpdateRequest": {
        "type": "object",
        "description": "A partial write: what the body does not name is left as it is. The object /v1/email-templates/get answers can be sent back as it is.",
        "required": [
          "email_template_key"
        ],
        "additionalProperties": false,
        "properties": {
          "email_template_key": {
            "type": "string",
            "description": "The template to change. From /v1/email-templates/list."
          },
          "name": {
            "description": "Left out or null, it is left as it is.",
            "minLength": 2,
            "maxLength": 50,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "description": "An empty string takes the note away; null leaves it as it is.",
            "maxLength": 250,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "format": {
            "description": "Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "plain_text",
                  "html"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "sender": {
            "description": "Left out or null, it is left as it is.",
            "minLength": 5,
            "maxLength": 70,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "sender_email": {
            "description": "The same rule as on a creation: an address of the account or its domain. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "reply_to_email": {
            "description": "The same. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "subject": {
            "description": "Left out or null, it is left as it is.",
            "minLength": 5,
            "maxLength": 150,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "body": {
            "description": "Replaces the whole body. Left out or null, it is left as it is.",
            "minLength": 5,
            "maxLength": 75000,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          }
        }
      },
      "EmailTemplatesUpdateData": {
        "type": "object",
        "description": "The template as it is now, and what it was. Sending `before` back through this endpoint puts it as it was; what it does not undo is an email a delivery sent in between.",
        "required": [
          "email_template",
          "before",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "email_template": {
            "$ref": "#/components/schemas/EmailTemplateWithBody"
          },
          "before": {
            "$ref": "#/components/schemas/EmailTemplateWithBody"
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "EmailTemplatesUpdateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/EmailTemplatesUpdateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "EmailTemplatesDeleteRequest": {
        "type": "object",
        "required": [
          "email_template_key"
        ],
        "additionalProperties": false,
        "properties": {
          "email_template_key": {
            "type": "string",
            "description": "The template to delete. From /v1/email-templates/list."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          },
          "confirmation_token": {
            "type": "string",
            "description": "The token the first call answered with, to apply the operation."
          }
        }
      },
      "EmailTemplatesDeleteData": {
        "type": "object",
        "description": "What the template was, which is all a deletion can give back.",
        "required": [
          "email_template_key",
          "before",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "email_template_key": {
            "type": "string"
          },
          "before": {
            "$ref": "#/components/schemas/EmailTemplateWithBody"
          },
          "deleted": {
            "type": "boolean",
            "description": "Present on the write. The preview carries permanent instead."
          },
          "permanent": {
            "type": "boolean",
            "description": "Present on the preview: the deletion has no inverse."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "EmailTemplatesDeleteResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/EmailTemplatesDeleteData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "LanguageFilesGetRequest": {
        "type": "object",
        "required": [
          "language_file_key"
        ],
        "additionalProperties": false,
        "properties": {
          "language_file_key": {
            "type": "string",
            "description": "The file. From /v1/language-files/list; the sixteen of the platform are named language_file_en, language_file_es and so on, and they can be read and downloaded."
          }
        }
      },
      "LanguageFilesGetData": {
        "type": "object",
        "required": [
          "language_file"
        ],
        "properties": {
          "language_file": {
            "$ref": "#/components/schemas/LanguageFileDetail"
          }
        }
      },
      "LanguageFilesGetResponse": {
        "type": "object",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/LanguageFilesGetData"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "LanguageFilesCreateRequest": {
        "type": "object",
        "description": "A new language file of the account, created from a .txt. Download one with /v1/language-files/get -- one of the sixteen the platform ships is the usual starting point -- edit the values and send it here.",
        "required": [
          "name",
          "language",
          "file"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 50,
            "description": "How the file is listed in the account. 2 to 50 characters, with no character above the Basic Multilingual Plane."
          },
          "language": {
            "type": "string",
            "description": "The ISO 639-1 code the literals are written in, and the language the AI summaries of every report using this file are written in. The list is published by /v1/capabilities under language_files.languages."
          },
          "internal_notes": {
            "description": "A note for the account's own staff, at most 250 characters.",
            "maxLength": 250,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "file": {
            "$ref": "#/components/schemas/LanguageFileUpload",
            "description": "The .txt of the literals. Required: a language file with no literals is a row nothing would read."
          },
          "language_file_key": {
            "description": "Accepted and ignored: part of what a read publishes and a creation has no column for. It comes back as a READ_ONLY_FIELD_IGNORED warning.",
            "readOnly": true,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "is_system": {
            "description": "Accepted and ignored: part of what a read publishes. It comes back as a READ_ONLY_FIELD_IGNORED warning.",
            "readOnly": true,
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "file_name": {
            "description": "Accepted and ignored: the name of the .txt is written through the file object. It comes back as a READ_ONLY_FIELD_IGNORED warning.",
            "readOnly": true,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "download_url": {
            "description": "Accepted and ignored: part of what a read publishes. It comes back as a READ_ONLY_FIELD_IGNORED warning.",
            "readOnly": true,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "download_url_expires_at": {
            "description": "Accepted and ignored: part of what a read publishes. It comes back as a READ_ONLY_FIELD_IGNORED warning.",
            "readOnly": true,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          }
        }
      },
      "LanguageFilesCreateData": {
        "type": "object",
        "description": "The file that was written. The literals are not published: download them with /v1/language-files/get.",
        "required": [
          "language_file",
          "ignored_keys",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "language_file": {
            "$ref": "#/components/schemas/LanguageFile"
          },
          "ignored_keys": {
            "type": "integer",
            "description": "How many keys of the .txt no field catalogue of the platform knows. They were dropped, exactly as the upload of the application drops them, and everything else was stored; over zero it comes with a LANGUAGE_FILE_KEYS_IGNORED warning. Present only when the call carried a file."
          },
          "created_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The key of the file, as every creation of this API publishes it."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "LanguageFilesCreateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/LanguageFilesCreateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "LanguageFilesUpdateRequest": {
        "type": "object",
        "description": "A partial write: what the body does not name is left as it is. `file` is the one field that is not a column -- with it every literal of the file is REPLACED, and without it not a single literal is touched.",
        "required": [
          "language_file_key"
        ],
        "additionalProperties": false,
        "properties": {
          "language_file_key": {
            "type": "string",
            "description": "The file to change. One of the account's own: the sixteen of the platform answer 409 NOT_ALLOWED."
          },
          "name": {
            "description": "Left out or null, it is left as it is.",
            "minLength": 2,
            "maxLength": 50,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "language": {
            "description": "The ISO 639-1 code of the literals. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "description": "An empty string takes the note away; null leaves it as it is.",
            "maxLength": 250,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "file": {
            "description": "A new .txt, which REPLACES every literal of the file. Left out, the literals are not touched. The previous file is not kept anywhere.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/LanguageFileUpload"
              },
              {
                "type": "null"
              }
            ]
          },
          "is_system": {
            "description": "Accepted and ignored: part of what a read publishes. It comes back as a READ_ONLY_FIELD_IGNORED warning.",
            "readOnly": true,
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "file_name": {
            "description": "Accepted and ignored: the name of the .txt is written through the file object. It comes back as a READ_ONLY_FIELD_IGNORED warning.",
            "readOnly": true,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "download_url": {
            "description": "Accepted and ignored: part of what a read publishes. It comes back as a READ_ONLY_FIELD_IGNORED warning.",
            "readOnly": true,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "download_url_expires_at": {
            "description": "Accepted and ignored: part of what a read publishes. It comes back as a READ_ONLY_FIELD_IGNORED warning.",
            "readOnly": true,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          }
        }
      },
      "LanguageFilesUpdateData": {
        "type": "object",
        "description": "The file as it is now, and what it was. `before` carries the columns and NOT the literals: the .txt a call replaced is not kept anywhere, so the inverse of an update that carried a file is uploading the previous file again.",
        "required": [
          "language_file",
          "before",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "language_file": {
            "$ref": "#/components/schemas/LanguageFile"
          },
          "ignored_keys": {
            "type": "integer",
            "description": "How many keys of the .txt no field catalogue of the platform knows. They were dropped, exactly as the upload of the application drops them, and everything else was stored; over zero it comes with a LANGUAGE_FILE_KEYS_IGNORED warning. Present only when the call carried a file."
          },
          "before": {
            "$ref": "#/components/schemas/LanguageFile"
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "LanguageFilesUpdateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/LanguageFilesUpdateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "LanguageFilesDeleteRequest": {
        "type": "object",
        "required": [
          "language_file_key"
        ],
        "additionalProperties": false,
        "properties": {
          "language_file_key": {
            "type": "string",
            "description": "The file to delete. One of the account's own."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          },
          "confirmation_token": {
            "type": "string",
            "description": "The token the first call answered with, to apply the operation."
          }
        }
      },
      "LanguageFilesDeleteData": {
        "type": "object",
        "description": "What the file was, which is all a deletion can give back: the literals go with the row.",
        "required": [
          "language_file_key",
          "before",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "language_file_key": {
            "type": "string"
          },
          "before": {
            "$ref": "#/components/schemas/LanguageFile"
          },
          "deleted": {
            "type": "boolean",
            "description": "Present on the write. The preview carries permanent instead."
          },
          "permanent": {
            "type": "boolean",
            "description": "Present on the preview: the deletion has no inverse."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "LanguageFilesDeleteResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/LanguageFilesDeleteData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "BrandTemplatesCreateRequest": {
        "type": "object",
        "description": "Every setting is optional: what a body does not carry takes the default of the product, and a new template is born with the logo of the platform.",
        "required": [
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "description": "A note for the account's own staff. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "font_family": {
            "description": "The font of the PDF. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "margin_top": {
            "description": "The top margin of the page, in points. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "margin_bottom": {
            "description": "The bottom margin of the page, in points. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "margin_left": {
            "description": "The left margin of the page, in points. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "margin_right": {
            "description": "The right margin of the page, in points. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_title_text_color": {
            "description": "The colour of the title of the cover, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_title_font_size": {
            "description": "The size of the title of the cover. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_title_font_weight": {
            "description": "The weight of the title of the cover. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "normal",
                  "bold"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "report_subtitle_text_color": {
            "description": "The colour of the subtitle of the cover, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_subtitle_font_size": {
            "description": "The size of the subtitle of the cover. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_subtitle_font_weight": {
            "description": "The weight of the subtitle of the cover. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "normal",
                  "bold"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "section_name_text_color": {
            "description": "The colour of the name of a section, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "section_name_font_size": {
            "description": "The size of the name of a section. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "section_name_font_weight": {
            "description": "The weight of the name of a section. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "normal",
                  "bold"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "date_range_text_color": {
            "description": "The colour of the period line of a section, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "date_range_font_size": {
            "description": "The size of the period line of a section. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "date_range_font_weight": {
            "description": "The weight of the period line of a section. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "normal",
                  "bold"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "component_name_text_color": {
            "description": "The colour of the name of a widget, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "component_name_font_size": {
            "description": "The size of the name of a widget. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "component_name_font_weight": {
            "description": "The weight of the name of a widget. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "normal",
                  "bold"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "footer_text_color": {
            "description": "The colour of the footer, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "footer_font_size": {
            "description": "The size of the footer. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "footer_font_weight": {
            "description": "The weight of the footer. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "normal",
                  "bold"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "include_logo": {
            "description": "Whether the report prints the logo. It cannot be true on a template that carries none, and setting it FALSE REMOVES the file, which is what the settings window of the application does when the checkbox is unticked: nothing keeps a copy, so switching it on again needs a new upload through /v1/brand-templates/set-logo. The answer says so with a LOGO_REMOVED warning. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "include_section_line": {
            "description": "Whether a line is drawn under the name of a section. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "section_line_color": {
            "description": "The colour of that line, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "component_name_background_color": {
            "description": "The background behind the name of a widget, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "component_content_text_color": {
            "description": "The colour of the content of a widget, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "component_content_font_size": {
            "description": "The size of the content of a widget. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "colors": {
            "description": "The palette the charts are drawn with, in order, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "footer_content": {
            "description": "The text of the footer. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "include_footer_line": {
            "description": "Whether a line is drawn above the footer. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "footer_line_color": {
            "description": "The colour of that line, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "primary_color": {
            "description": "The main colour of the brand, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "cover_style": {
            "description": "The style the front cover is drawn with. A template that never picked one is read as plain, which is what it is drawn with. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "gradient",
                  "plain",
                  "aurora",
                  "circles",
                  "waves",
                  "diagonal",
                  "rings",
                  "dots",
                  "bubbles",
                  "contours",
                  "blocks",
                  "image"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "cover_tone": {
            "description": "The tone of the text of the cover. auto takes it from the primary colour and is what a template that never picked one is read as. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "auto",
                  "light",
                  "dark",
                  "custom"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "cover_text_color": {
            "description": "The colour of the cover text when the tone is custom, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "cover_inverted": {
            "description": "Whether the cover is drawn on a coloured surface instead of white paper. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "prepared_by": {
            "description": "The line naming who prepared the report. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "prepared_for": {
            "description": "The line naming who it was prepared for. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "toc_style": {
            "description": "The style of the table of contents. A template that never picked one is read as timeline. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "cards",
                  "rule",
                  "band",
                  "timeline"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "portal_header_style": {
            "description": "The header the client portal wears. none draws no header and is what a template that never picked one is read as. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "none",
                  "letterhead",
                  "card",
                  "band"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "portal_header_logo": {
            "description": "Whether that header carries the logo. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "web_font_family": {
            "description": "The font of the client portal and of a public link. app_default is the font of the application. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "brand_template_key": {
            "description": "Ignored on a write: part of what a read publishes and not part of what a write stores. It is accepted so the object /v1/brand-templates/get answers can be sent straight back, and it comes back as a READ_ONLY_FIELD_IGNORED warning.",
            "readOnly": true,
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "is_system": {
            "description": "Ignored on a write: part of what a read publishes and not part of what a write stores. It is accepted so the object /v1/brand-templates/get answers can be sent straight back, and it comes back as a READ_ONLY_FIELD_IGNORED warning.",
            "readOnly": true,
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "logo": {
            "description": "Ignored on a write: part of what a read publishes and not part of what a write stores. It is accepted so the object /v1/brand-templates/get answers can be sent straight back, and it comes back as a READ_ONLY_FIELD_IGNORED warning.",
            "readOnly": true,
            "anyOf": [
              {
                "type": "object"
              },
              {
                "type": "null"
              }
            ]
          },
          "cover_image": {
            "description": "Ignored on a write: part of what a read publishes and not part of what a write stores. It is accepted so the object /v1/brand-templates/get answers can be sent straight back, and it comes back as a READ_ONLY_FIELD_IGNORED warning.",
            "readOnly": true,
            "anyOf": [
              {
                "type": "object"
              },
              {
                "type": "null"
              }
            ]
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          }
        }
      },
      "BrandTemplatesUpdateRequest": {
        "type": "object",
        "description": "A partial update: the fields the body carries are written and every other column of the template, the two images included, is left as it is.",
        "required": [
          "brand_template_key"
        ],
        "additionalProperties": false,
        "properties": {
          "brand_template_key": {
            "type": "string",
            "description": "The brand template to write. From /v1/brand-templates/list. The empty key is the brand of the platform and is refused with 409 NOT_ALLOWED."
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "internal_notes": {
            "description": "A note for the account's own staff. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "font_family": {
            "description": "The font of the PDF. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "margin_top": {
            "description": "The top margin of the page, in points. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "margin_bottom": {
            "description": "The bottom margin of the page, in points. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "margin_left": {
            "description": "The left margin of the page, in points. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "margin_right": {
            "description": "The right margin of the page, in points. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_title_text_color": {
            "description": "The colour of the title of the cover, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_title_font_size": {
            "description": "The size of the title of the cover. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_title_font_weight": {
            "description": "The weight of the title of the cover. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "normal",
                  "bold"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "report_subtitle_text_color": {
            "description": "The colour of the subtitle of the cover, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_subtitle_font_size": {
            "description": "The size of the subtitle of the cover. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_subtitle_font_weight": {
            "description": "The weight of the subtitle of the cover. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "normal",
                  "bold"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "section_name_text_color": {
            "description": "The colour of the name of a section, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "section_name_font_size": {
            "description": "The size of the name of a section. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "section_name_font_weight": {
            "description": "The weight of the name of a section. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "normal",
                  "bold"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "date_range_text_color": {
            "description": "The colour of the period line of a section, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "date_range_font_size": {
            "description": "The size of the period line of a section. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "date_range_font_weight": {
            "description": "The weight of the period line of a section. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "normal",
                  "bold"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "component_name_text_color": {
            "description": "The colour of the name of a widget, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "component_name_font_size": {
            "description": "The size of the name of a widget. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "component_name_font_weight": {
            "description": "The weight of the name of a widget. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "normal",
                  "bold"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "footer_text_color": {
            "description": "The colour of the footer, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "footer_font_size": {
            "description": "The size of the footer. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "footer_font_weight": {
            "description": "The weight of the footer. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "normal",
                  "bold"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "include_logo": {
            "description": "Whether the report prints the logo. It cannot be true on a template that carries none, and setting it FALSE REMOVES the file, which is what the settings window of the application does when the checkbox is unticked: nothing keeps a copy, so switching it on again needs a new upload through /v1/brand-templates/set-logo. The answer says so with a LOGO_REMOVED warning. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "include_section_line": {
            "description": "Whether a line is drawn under the name of a section. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "section_line_color": {
            "description": "The colour of that line, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "component_name_background_color": {
            "description": "The background behind the name of a widget, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "component_content_text_color": {
            "description": "The colour of the content of a widget, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "component_content_font_size": {
            "description": "The size of the content of a widget. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "colors": {
            "description": "The palette the charts are drawn with, in order, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "footer_content": {
            "description": "The text of the footer. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "include_footer_line": {
            "description": "Whether a line is drawn above the footer. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "footer_line_color": {
            "description": "The colour of that line, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "primary_color": {
            "description": "The main colour of the brand, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "cover_style": {
            "description": "The style the front cover is drawn with. A template that never picked one is read as plain, which is what it is drawn with. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "gradient",
                  "plain",
                  "aurora",
                  "circles",
                  "waves",
                  "diagonal",
                  "rings",
                  "dots",
                  "bubbles",
                  "contours",
                  "blocks",
                  "image"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "cover_tone": {
            "description": "The tone of the text of the cover. auto takes it from the primary colour and is what a template that never picked one is read as. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "auto",
                  "light",
                  "dark",
                  "custom"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "cover_text_color": {
            "description": "The colour of the cover text when the tone is custom, as #RRGGBB. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "cover_inverted": {
            "description": "Whether the cover is drawn on a coloured surface instead of white paper. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "prepared_by": {
            "description": "The line naming who prepared the report. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "prepared_for": {
            "description": "The line naming who it was prepared for. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "toc_style": {
            "description": "The style of the table of contents. A template that never picked one is read as timeline. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "cards",
                  "rule",
                  "band",
                  "timeline"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "portal_header_style": {
            "description": "The header the client portal wears. none draws no header and is what a template that never picked one is read as. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "none",
                  "letterhead",
                  "card",
                  "band"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "portal_header_logo": {
            "description": "Whether that header carries the logo. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "web_font_family": {
            "description": "The font of the client portal and of a public link. app_default is the font of the application. Left out or null, it is left as it is.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "is_system": {
            "description": "Ignored on a write: part of what a read publishes and not part of what a write stores. It is accepted so the object /v1/brand-templates/get answers can be sent straight back, and it comes back as a READ_ONLY_FIELD_IGNORED warning.",
            "readOnly": true,
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "logo": {
            "description": "Ignored on a write: part of what a read publishes and not part of what a write stores. It is accepted so the object /v1/brand-templates/get answers can be sent straight back, and it comes back as a READ_ONLY_FIELD_IGNORED warning.",
            "readOnly": true,
            "anyOf": [
              {
                "type": "object"
              },
              {
                "type": "null"
              }
            ]
          },
          "cover_image": {
            "description": "Ignored on a write: part of what a read publishes and not part of what a write stores. It is accepted so the object /v1/brand-templates/get answers can be sent straight back, and it comes back as a READ_ONLY_FIELD_IGNORED warning.",
            "readOnly": true,
            "anyOf": [
              {
                "type": "object"
              },
              {
                "type": "null"
              }
            ]
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          }
        }
      },
      "BrandTemplatesDeleteRequest": {
        "type": "object",
        "required": [
          "brand_template_key"
        ],
        "additionalProperties": false,
        "properties": {
          "brand_template_key": {
            "type": "string",
            "description": "The brand template to delete. From /v1/brand-templates/list. The empty key is the brand of the platform and is refused with 409 NOT_ALLOWED."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          },
          "confirmation_token": {
            "type": "string",
            "description": "The token the first call answered with, to apply the operation."
          }
        }
      },
      "BrandTemplatesSetLogoRequest": {
        "type": "object",
        "required": [
          "brand_template_key",
          "file"
        ],
        "additionalProperties": false,
        "properties": {
          "brand_template_key": {
            "type": "string",
            "description": "The brand template whose logo is written. The empty key is the brand of the platform and is refused with 409 NOT_ALLOWED."
          },
          "file": {
            "description": "The image, or null to remove the one the brand template carries.",
            "anyOf": [
              {
                "type": "object",
                "required": [
                  "name",
                  "content_base64"
                ],
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "The name of the file, ending in one of: .jpg, .jpeg, .png or .gif. It is read so a caller that sent the wrong file hears which one; the first bytes are what decide the format."
                  },
                  "content_base64": {
                    "type": "string",
                    "description": "The image itself, base64 encoded, with no data URI prefix."
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          }
        }
      },
      "BrandTemplatesSetCoverImageRequest": {
        "type": "object",
        "required": [
          "brand_template_key",
          "file"
        ],
        "additionalProperties": false,
        "properties": {
          "brand_template_key": {
            "type": "string",
            "description": "The brand template whose cover image is written. The empty key is the brand of the platform and is refused with 409 NOT_ALLOWED."
          },
          "file": {
            "description": "The image, or null to remove the one the brand template carries.",
            "anyOf": [
              {
                "type": "object",
                "required": [
                  "name",
                  "content_base64"
                ],
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "The name of the file, ending in one of: .jpg, .jpeg or .png. The first bytes are what decide the format, with one exception: a name ending in .gif is refused on the spot with details.reason format_not_supported, which is the same answer its bytes would have got."
                  },
                  "content_base64": {
                    "type": "string",
                    "description": "The photograph itself, base64 encoded, with no data URI prefix."
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate everything and write nothing."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Replays the answer of an identical call for 24 hours."
          }
        }
      },
      "BrandTemplatesCreateData": {
        "type": "object",
        "description": "The brand template that was written, read back in the shape /v1/brand-templates/get publishes.",
        "required": [
          "brand_template",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "brand_template": {
            "$ref": "#/components/schemas/BrandTemplate"
          },
          "created_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The key of the brand template, as every creation of this API publishes it."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "BrandTemplatesUpdateData": {
        "type": "object",
        "description": "The brand template as it is now, and what it was. Sending `before` back through this endpoint puts every setting as it was; the two images are not part of it, because no write of settings touches them.",
        "required": [
          "brand_template",
          "before",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "brand_template": {
            "$ref": "#/components/schemas/BrandTemplate"
          },
          "before": {
            "$ref": "#/components/schemas/BrandTemplate"
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "BrandTemplatesDeleteData": {
        "type": "object",
        "description": "What the brand template was, which is all a deletion can give back.",
        "required": [
          "brand_template_key",
          "before",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "brand_template_key": {
            "type": "string"
          },
          "before": {
            "$ref": "#/components/schemas/BrandTemplate"
          },
          "deleted": {
            "type": "boolean",
            "description": "Present on the write. The preview carries permanent instead."
          },
          "permanent": {
            "type": "boolean",
            "description": "Present on the preview: the deletion has no inverse, and the two images go with the row."
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "BrandTemplatesSetLogoData": {
        "type": "object",
        "description": "Whether the brand template carries a logo now, and whether it did.",
        "required": [
          "brand_template_key",
          "logo",
          "before",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "brand_template_key": {
            "type": "string"
          },
          "logo": {
            "description": "A logo after the call, or null.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/BrandTemplateImage"
              },
              {
                "type": "null"
              }
            ]
          },
          "before": {
            "type": "object",
            "description": "What the template carried. The file itself is not kept anywhere, so this says only whether there was one.",
            "required": [
              "logo"
            ],
            "properties": {
              "logo": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/BrandTemplateImage"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "BrandTemplatesSetCoverImageData": {
        "type": "object",
        "description": "Whether the brand template carries a cover image now, and whether it did.",
        "required": [
          "brand_template_key",
          "cover_image",
          "before",
          "unfulfilled",
          "warnings"
        ],
        "properties": {
          "brand_template_key": {
            "type": "string"
          },
          "cover_image": {
            "description": "A cover image after the call, or null.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/BrandTemplateImage"
              },
              {
                "type": "null"
              }
            ]
          },
          "before": {
            "type": "object",
            "description": "What the template carried. The file itself is not kept anywhere, so this says only whether there was one.",
            "required": [
              "cover_image"
            ],
            "properties": {
              "cover_image": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/BrandTemplateImage"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          },
          "unfulfilled": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "BrandTemplatesCreateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/BrandTemplatesCreateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "BrandTemplatesUpdateResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/BrandTemplatesUpdateData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "BrandTemplatesDeleteResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/BrandTemplatesDeleteData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "BrandTemplatesSetLogoResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/BrandTemplatesSetLogoData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      },
      "BrandTemplatesSetCoverImageResponse": {
        "type": "object",
        "description": "The 200 of this write, in whichever of its shapes the call asked for.",
        "required": [
          "status",
          "data",
          "meta"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "data": {
            "description": "The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead, and the call that asks for a confirmation answers WriteConfirmationRequiredData. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/BrandTemplatesSetCoverImageData"
              },
              {
                "$ref": "#/components/schemas/WriteDryRunData"
              },
              {
                "$ref": "#/components/schemas/WriteConfirmationRequiredData"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/WriteMeta"
          }
        }
      }
    },
    "responses": {
      "RateLimitExceeded": {
        "description": "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.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds until the more-restrictive window resets."
          },
          "X-RateLimit-Limit": {
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          "X-RateLimit-Limit-Day": {
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining-Day": {
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset-Day": {
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "status": "error",
              "error_code": "RATE_LIMITED",
              "message": "Rate limit exceeded. Retry after 42 seconds.",
              "meta": {
                "request_id": "req_abc123",
                "timestamp": "2026-05-26T10:30:00Z"
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Invalid or missing Bearer token.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "status": "error",
              "error_code": "AUTH_INVALID",
              "message": "Invalid API key",
              "meta": {
                "request_id": "req_abc123",
                "timestamp": "2026-05-16T10:30:00Z"
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Internal server error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "BadRequest": {
        "description": "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.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "status": "error",
              "error_code": "VALIDATION_FAILED",
              "message": "The request did not pass validation: 2 problems",
              "details": {
                "problems": [
                  {
                    "path": "sections[0].widgets[0].dsl",
                    "code": "DSL_INVALID",
                    "message": "root.metrics: missing (every field of the schema is required)"
                  },
                  {
                    "path": "name",
                    "code": "INVALID_VALUE",
                    "message": "a name is at most 255 characters and this one is 402"
                  }
                ]
              },
              "meta": {
                "request_id": "req_a1b2c3d4e5f6",
                "timestamp": "2026-09-18T10:30:00Z"
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "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.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "status": "error",
              "error_code": "WRITE_ACCESS_DISABLED",
              "message": "Write access is not enabled for this account. Enable it in the REST API tab of the app.",
              "meta": {
                "request_id": "req_a1b2c3d4e5f6",
                "timestamp": "2026-09-18T10:30:00Z"
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "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.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "status": "error",
              "error_code": "NOT_FOUND",
              "message": "No report with key 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6'",
              "meta": {
                "request_id": "req_a1b2c3d4e5f6",
                "timestamp": "2026-09-18T10:30:00Z"
              }
            }
          }
        }
      },
      "Conflict": {
        "description": "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.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds to wait before retrying."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "status": "error",
              "error_code": "VERSION_MISMATCH",
              "message": "The report has moved on since if_version was read",
              "details": {
                "expected": 7,
                "actual": 9
              },
              "meta": {
                "request_id": "req_a1b2c3d4e5f6",
                "timestamp": "2026-09-18T10:30:00Z"
              }
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "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.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "status": "error",
              "error_code": "PAYLOAD_TOO_LARGE",
              "message": "Request body exceeds 1 MB",
              "meta": {
                "request_id": "req_a1b2c3d4e5f6",
                "timestamp": "2026-09-18T10:30:00Z"
              }
            }
          }
        }
      },
      "WidgetExecutionFailed": {
        "description": "verify_data ran the widget against the provider and the provider refused it. Nothing was created; `details` carries the verdict of the run.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "status": "error",
              "error_code": "WIDGET_EXECUTION_FAILED",
              "message": "The metric 'cost_per_conversion' is not available for this data view",
              "details": {
                "verdict": "failure",
                "message": "The metric 'cost_per_conversion' is not available for this data view",
                "reason": null
              },
              "meta": {
                "request_id": "req_a1b2c3d4e5f6",
                "timestamp": "2026-09-18T10:30:00Z"
              }
            }
          }
        }
      },
      "NotificationFailed": {
        "description": "The notification email could not be sent, and because it is sent inside the transaction that writes the view, the write was rolled back with it. Nothing changed; the call is worth retrying.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "status": "error",
              "error_code": "NOTIFICATION_FAILED",
              "message": "The notification email could not be sent and the portal view was not written; retry",
              "meta": {
                "request_id": "req_a1b2c3d4e5f6",
                "timestamp": "2026-09-18T10:30:00Z"
              }
            }
          }
        }
      },
      "WriteTemporarilyUnavailable": {
        "description": "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.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "status": "error",
              "error_code": "WRITE_TEMPORARILY_UNAVAILABLE",
              "message": "Writes are temporarily unavailable; retry later",
              "meta": {
                "request_id": "req_a1b2c3d4e5f6",
                "timestamp": "2026-09-18T10:30:00Z"
              }
            }
          }
        }
      }
    },
    "examples": {
      "WriteDryRunInvalid": {
        "summary": "dry_run: true with a body that would not be accepted. HTTP 200, nothing written.",
        "value": {
          "status": "ok",
          "data": {
            "dry_run": true,
            "valid": false,
            "message": "The request did not pass validation: 2 problems",
            "problems": [
              {
                "path": "sections[0].widgets[0].dsl",
                "code": "DSL_INVALID",
                "message": "root.metrics: missing (every field of the schema is required)"
              },
              {
                "path": "data_sources[1].account_id",
                "code": "ACCOUNT_NOT_FOUND",
                "message": "the connection does not reach an account with id '000000000'"
              }
            ]
          },
          "meta": {
            "request_id": "req_a1b2c3d4e5f6",
            "timestamp": "2026-09-18T10:30:00Z"
          }
        }
      },
      "WriteConfirmationRequired": {
        "summary": "The first call to an endpoint that confirms. HTTP 200, nothing written; send the same body again with the token. The preview of the operation travels alongside these three fields.",
        "value": {
          "status": "ok",
          "data": {
            "confirmation_required": true,
            "confirmation_token": "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce",
            "expires_at": "2026-09-18T10:40:00Z"
          },
          "meta": {
            "request_id": "req_a1b2c3d4e5f6",
            "timestamp": "2026-09-18T10:30:00Z"
          }
        }
      },
      "WriteReplayed": {
        "summary": "A retry with the same idempotency_key and the same body. The stored answer comes back with the meta of THIS request and the id of the call that did the work.",
        "value": {
          "status": "ok",
          "data": {
            "report_key": "9f3c1a7e42b84d0596ce1f8a3b7d2e64",
            "deleted": true,
            "unfulfilled": [],
            "warnings": []
          },
          "meta": {
            "request_id": "req_9z8y7x6w5v4u",
            "timestamp": "2026-09-18T10:31:12Z",
            "replayed_from_request_id": "req_a1b2c3d4e5f6",
            "replayed": true
          }
        }
      }
    }
  }
}
