---
title: "Run a data query"
method: POST
path: "/query"
tags: ["Query"]
---

# Run a data query

`POST /query`

## Request

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

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.

## Request body

- QueryRequest
  - `integration_id` string, required
  - `connection_key` string, required
  - `account_id` string, required
  - `data_view` string
  - `fields` string[], required
  - `date_range` DateRange, required
    - `preset` 'today' | 'yesterday' | 'lastweeksunsat' | 'lastweekmonsun' | 'thismonth' | 'thismonthtoyesterday' | 'lastmonth' | 'thisyear' | 'lastyear' | 'lastyeartodate' | 'lastxdays' | 'lastxweeks' | 'lastxweeksiso' | 'lastxmonths' | 'lastxyears' | 'custom', required
    - `x` integer: Required for lastx* presets.
    - `include_current` boolean: Optional for lastxmonths.
    - `start` string: 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` string: 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.
  - `compare_to` CompareRange
    - `preset` 'none' | 'previous' | 'year_ago' | 'year_ago_match_weekday' | 'custom', required
    - `start` string, date: Required for custom.
    - `end` string, date: Required for custom.
  - `comparison_type` 'percentage' | 'absolute' | 'value'
  - `settings` object: 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` Filter[]
    - `field` string, required: 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` string, required: 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` unknown, required
    - `type_of_field` 'dimension' | 'metric': 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` 'AND' | 'OR': 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).
  - `limit` integer, required: 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` string, nullable: 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.
  - `bypass_cache` boolean: 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.

## Response `200`

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.

- union
  - QueryResponse
    - `status` 'ok', required
    - `data` QueryData, required: 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.
      - `rows` object[], required: One object per row, keyed by the field ids that were asked for.
    - `meta` QueryMeta, required: The meta of a query answer: the base fields plus what the query resolved and how much of it came back.
      - `request_id` string, required
      - `timestamp` string, date-time, required
      - `total_rows` integer, required: Rows the query matched.
      - `returned_rows` integer, required: Rows in this page.
      - `has_more` boolean, required: Whether another page is available.
      - `next_cursor` string: Cursor for the next page. Present only when has_more is true.
      - `date_range_resolved` object, required: The period the preset resolved to, as yyyy-MM-dd.
        - `start` string, required
        - `end` string, required
      - `compare_date_range_resolved` object: The comparison period. Present only when compare_to was sent.
        - `start` string, required
        - `end` string, required
      - `comparison_type` 'percentage' | 'absolute' | 'value': How the comparison columns are expressed. Present only when compare_to was sent.
  - ErrorEnvelope
    - `status` 'error', required
    - `error_code` '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', required: 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.
    - `message` string, required: Human-readable error description. Useful for logs and UIs; not stable across versions -- use `error_code` for programmatic handling.
    - `meta` WriteMeta, required: The meta of a write answer. A replay carries two extra fields so it can be told apart from a first call and still traced back to it.
      - `request_id` string, required: Id of THIS request, including on a replay.
      - `timestamp` string, date-time, required
      - `replayed` boolean: True when this answer is the stored answer of an earlier call with the same idempotency_key and the same body. Absent on a first call.
      - `replayed_from_request_id` string, nullable: The request_id of the call that actually did the work. Present only on a replay, and null when the stored answer carried no id.
    - `details` object: 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).
      - `problems` Problem[]
        - `path` string, required: Dotted path of the offending field inside the request body, empty for a problem about the body as a whole.
        - `code` 'MISSING_REQUIRED_FIELD' | 'INVALID_VALUE' | 'UNKNOWN_PARAM' | 'INVALID_DATE_RANGE' | 'INTEGRATION_NOT_SUPPORTED' | 'CONNECTION_NOT_FOUND' | 'ACCOUNT_NOT_FOUND' | 'ONE_CONNECTION_PER_INTEGRATION' | 'DATE_PRESET_NOT_IN_REPORTS' | 'TEMPLATE_NOT_FOUND' | 'TEMPLATE_PARAMETERIZED' | 'ROW_RULE' | 'POSITION_CHANGED' | 'SECTION_CHANGED' | 'TOO_MANY_PROBLEMS' | 'DATA_SOURCES_PARTIALLY_BOUND' | 'WELCOME_EMAIL_FAILED' | 'REPORT_WILL_BE_SHARED_PUBLICLY' | 'MAY_BE_DELIVERED_AGAIN_TODAY' | 'SUMMARY_SECTION_SKIPPED' | 'COVER_FILE_REMOVED' | 'COVER_TYPE_SET' | 'LOGO_REMOVED' | 'READ_ONLY_FIELD_IGNORED' | 'AI_BRIEF_NOT_SAVED' | 'SPENDS_AI_CREDITS' | 'DROPS_ACCOUNT_CACHE' | 'CHART_TYPE_NOT_FOR_THIS_SHAPE' | 'CHART_TYPE_NOT_FOR_TREND' | 'CONTENT_NOT_FOR_THIS_SHAPE' | 'FULL_WIDTH_NOT_FOR_KPI' | 'DATA_VIEW_FIELDS_PER_ACCOUNT' | 'DATA_VIEW_NOT_AVAILABLE' | 'DATA_VIEW_NOT_EMPTY' | 'DATA_VIEW_NOT_OF_THE_SECTION' | 'DATA_VIEW_REQUIRED' | 'DATA_VIEW_UNKNOWN' | 'DATE_RANGE_CUSTOM_INCOMPLETE' | 'DATE_RANGE_CUSTOM_INVALID' | 'DATE_RANGE_CUSTOM_INVERTED' | 'DATE_RANGE_PRESET_UNKNOWN' | 'DATE_RANGE_X_REQUIRED' | 'DIMENSION_DUPLICATED' | 'DIMENSION_IS_A_METRIC' | 'DIMENSION_REQUIRED' | 'DIMENSION_UNKNOWN' | 'DSL_INVALID' | 'FIELD_NOT_IN_AD_TYPES' | 'FILTERS_NOT_SUPPORTED' | 'FILTER_COMBINATION_NOT_SUPPORTED' | 'FILTER_CONDITIONS_MIXED_KINDS' | 'FILTER_FIELD_NOT_A_COLUMN' | 'FILTER_FIELD_NOT_IN_DATA_VIEW' | 'FILTER_FIELD_UNKNOWN' | 'FILTER_LIKE_NEEDS_WILDCARD' | 'FILTER_OPERATOR_INVALID' | 'FILTER_TYPE_OF_FIELD_MISMATCH' | 'FILTER_TYPE_OF_FIELD_NOT_USED' | 'FILTER_TYPE_OF_FIELD_REQUIRED' | 'FILTER_VALUE_EMPTY' | 'FILTER_VALUE_NOT_ALLOWED' | 'FILTER_VALUE_NOT_NUMERIC' | 'GRAPH_EXTRA_DIMENSIONS' | 'GRAPH_TIME_DIMENSION_MIXED' | 'INTEGRATION_UNKNOWN' | 'INTENT_MISSING' | 'KPI_NEEDS_ONE_METRIC' | 'KPI_TAKES_NO_DIMENSIONS' | 'LIMIT_IGNORED_IN_TREND' | 'LIMIT_OUT_OF_RANGE' | 'METRICS_SUMMARY_TAKES_NO_DIMENSIONS' | 'METRIC_DUPLICATED' | 'METRIC_IS_A_DIMENSION' | 'METRIC_UNKNOWN' | 'METRICS_REQUIRED' | 'NAME_EMPTY' | 'NAME_NOT_PLAIN_TEXT' | 'NAME_TOO_LONG' | 'PIE_TAKES_ONE_DIMENSION' | 'PREVIOUS_PERIOD_NOT_AVAILABLE_FOR_THIS_TABLE' | 'COMPARISON_NOT_AVAILABLE_FOR_THIS_WIDGET' | 'PREVIOUS_PERIOD_NOT_FOR_THIS_SHAPE' | 'SHOW_CHANGE_NOT_FOR_THIS_SHAPE' | 'SHOW_CHANGE_NEEDS_PREVIOUS_PERIOD' | 'SETTING_DUPLICATED' | 'SETTING_NOT_FOR_DATA_VIEW' | 'SETTING_NOT_IN_WIDGET' | 'SETTING_UNKNOWN' | 'SETTING_VALUE_INVALID' | 'SORT_FIELD_NOT_SELECTED' | 'TABLE_TIME_DIMENSION_NOT_DRAWABLE' | 'TEXT_CONTENT_REQUIRED' | 'TEXT_CONTENT_TOO_LONG' | 'TEXT_NOT_A_WIDGET_TYPE' | 'TEXT_TAKES_NO_DATA' | 'TOO_MANY_DIMENSIONS' | 'TOO_MANY_METRICS' | 'TRANSLATION_FAILED' | 'TREND_ORDER_IS_THE_TIME_AXIS' | 'TREND_TAKES_NO_DIMENSIONS' | 'VISUALIZATION_NOT_AVAILABLE' | 'VISUALIZATION_NOT_OF_REQUESTED_TYPE' | 'FIELD_NOT_AVAILABLE' | 'FILTER_CONDITIONS_MIXED_LEVELS' | 'FILTER_LEVEL_MISMATCH' | 'FORMULA_TOKEN_UNKNOWN' | 'FORMULA_INVALID' | 'LANGUAGE_FILE_KEYS_IGNORED' | 'SELECTION_EMPTY' | 'FILTER_KEY_AND_FILTERS' | 'FILTER_KEY_NOT_FOR_THIS_SHAPE' | 'KEY_NOT_FOR_GENERATION' | 'COMPARE_NOT_FOR_THIS_SHAPE' | 'COMPARE_NOT_FOR_INTEGRATION' | 'COMPARE_ITEMS_CARDINALITY' | 'COMPARE_METRICS_CARDINALITY' | 'COMPARE_OVER_TIME_NOT_AVAILABLE' | 'COMPARE_METRIC_HAS_NO_BREAKDOWN' | 'COMPARE_KEY_NOT_FOR_THIS_COMPARATOR' | 'COMPARATOR_CLASS_FIXED', required: What is wrong with that field, or what is worth knowing about it. The first group is the vocabulary of the body itself; the eleven that follow it appear only in `warnings[]`, bar TOO_MANY_PROBLEMS, which closes a `problems` list that was truncated; and the rest are the codes of the widget DSL, which travel here when a composition or a widget carries a dsl that does not validate.
        - `message` string, required: Human-readable explanation. Not stable across versions.
        - `suggestions` string[]: The accepted values, when the field takes a closed list.
      - `used_by` union: 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.
        - EntityUsage: 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.
          - `reports` EntityUsageReport[], required
            - `report_key` string, nullable, required: 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` string, nullable, required: 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` boolean, required: Whether the report is in the trash. A report in the trash still reads the definition.
            - `widgets` integer, required: How many widgets of it read the definition.
            - `sections` integer, required: How many sections of it read the definition as a global filter.
          - `calculated_metrics` EntityUsageEntity[]: Only for a saved filter: the filtered metrics built on it.
            - `calculated_metric_key` string: Present in calculated_metrics.
            - `portal_view_key` string: Present in portal_views.
            - `name` string, nullable: What it is called.
          - `portal_views` EntityUsageEntity[]: Only for a saved filter: the portal views that hand it to their clients.
            - `calculated_metric_key` string: Present in calculated_metrics.
            - `portal_view_key` string: Present in portal_views.
            - `name` string, nullable: What it is called.
          - `alerts` integer: Only for a saved filter: how many alerts of the account are evaluated with it.
          - `account_groups` integer, required: How many Data Studio account groups read it.
        - EntityUsageSchedules: What still sends an email template, published inside details.used_by of an IN_USE.
          - `schedules` EntityUsageSchedule[], required
            - `schedule_key` string, required: The delivery, as /v1/schedules/list names it.
            - `report_key` string, nullable, required: The report it delivers.
            - `frequency` string, nullable, required: How often it goes out.
          - `client_portal` boolean, required: Whether the client portal of the account sends its notifications with this template, which is chosen in the application.
        - EntityUsageReports: What still reads a language file, published inside details.used_by of an IN_USE.
          - `reports` EntityUsageReportRef[], required
            - `report_key` string, required
            - `internal_name` string, nullable, required: What the report is called inside the account.
            - `in_trash` boolean, required: Whether the report is in the trash. A report in the trash still points at the file and would come back pointing at nothing.
      - `reason` string, nullable: 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.

## Other responses

- `400`: The request was refused before anything was written. INVALID_REQUEST covers the framework refusals of every endpoint (the Content-Type is not application/json, the body could not be read, the body is not valid JSON). The report endpoints add their own: VALIDATION_FAILED with details.problems, UNKNOWN_PARAM with the path of the field the endpoint does not read, and the single-field refusals MISSING_REQUIRED_FIELD, INVALID_VALUE and APPEND_NOT_ALLOWED. The description of each operation lists the ones it emits. Everything else a body can get wrong arrives as an entry of details.problems rather than as the code of the envelope.
- `401`: Invalid or missing Bearer token.
- `413`: The request body is over 1 MB. The cap is applied before authentication, so this answer is refused without the key being read, and it carries the usual error envelope.
- `429`: A budget was spent. RATE_LIMITED is the per-key read budget (300/minute, 5000/day) or the per-account write budget (60/minute, 1000/day). EMAIL_QUOTA_REACHED is the separate budget of 50 calls per account per day that send email, and its Retry-After points at the next UTC midnight. The Retry-After header gives the seconds until the more restrictive window resets. The X-RateLimit-* headers report the read budget, except on the 429 of a write, where they report the write budget.
- `500`: Internal server error.

---

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