# Reporting API V3 — Developer Guide

- **Applies to:** Sprinklr Reporting API, version 3 (`/api/v3/reports/*`), OpenAPI tag `Reporting V3`
- **V2 API reference:** [Reporting](https://dev.sprinklr.com/reporting) (index) — Custom Query, Custom Query Using Widget Id, Batch Query, Fetch Reporting Engines, Fetch Report Names, Fetch Custom Metrics, Fetch Metrics and Dimensions
- **Source ticket:** [IN-12915 — "CRUD for Reporting APIs"](https://sprinklr.atlassian.net/browse/IN-12915) (Story, **Closed / Done**, fix version **26.7**, parent epic [IN-12622](https://sprinklr.atlassian.net/browse/IN-12622) "V3 parity APIs all entities")
- **OpenAPI source:** `sprinklr-v3.yaml`, tag `Reporting V3`
- **Related Help Center:** Reporting Blueprints · Fetching Data through API Payload


> **Documentation status.** This guide covers the seven Reporting V3 endpoints enumerated in the IN-12915 description. The ticket is **Closed / Done**, was merged to `release-26.7-app`, and the assignee confirmed on 2026-06-29 that the endpoints are **"Working fine in prod0 as well."**
However, **no endpoint specification documents were supplied for this guide**, and the Postman collection attached to IN-12915 is stored as a Jira attachment that is not reachable from this environment. Every request and response example below is therefore **reconstructed from the V3 OpenAPI document plus the V2 reference pages** — none has been copied from a QA-verified V3 transcript. Treat the shapes as accurate and the literal values as illustrative.
A second, more significant caveat: **the V3 OpenAPI document encodes almost none of the reporting semantics.** Not one reporting schema property carries a description, a `required` marker, or an enum. Everything a developer actually needs to construct a working query — which fields are mandatory, what `filterType` accepts, what `groupType` accepts — exists **only in the V2 documentation**. Carrying those semantics forward is the main purpose of this guide, and each place where the V3 contract is silent is flagged with a **Specification gap** callout.


## Table of contents

| § | Section |
|  --- | --- |
| [0](#0-before-you-start) | Before you start |
| [1](#1-overview) | Overview — dashboards, widgets, engines, reports |
| [2](#2-base-urls-and-environments) | Base URLs and environments |
| [3](#3-authentication-and-common-headers) | Authentication and common headers |
| [4](#4-get-reportsengine--list-reporting-engines) | `GET /reports/engine` — List reporting engines |
| [5](#5-get-reportsreport--list-report-names) | `GET /reports/report` — List report names |
| [6](#6-get-reportsmetadata--fetch-metrics-and-dimensions) | `GET /reports/metadata` — Fetch metrics and dimensions |
| [7](#7-get-reportscustom-metric--fetch-custom-metrics) | `GET /reports/custom-metric` — Fetch custom metrics |
| [8](#8-post-reportsquery--custom-query) | `POST /reports/query` — Custom query |
| [9](#9-post-reportsquerywidget--custom-query-using-widget-id) | `POST /reports/query/widget` — Custom query using widget id |
| [10](#10-post-reportsbatchquery--batch-query) | `POST /reports/batchQuery` — Batch query |
| [11](#11-shared-request-objects) | Shared request objects and reference enums |
| [12](#12-response-format-and-status-codes) | Response format and status codes |
| [13](#13-v2--v3-migration) | V2 → V3 migration |
| [14](#14-related-reporting-v3-endpoints-not-covered-above) | Related Reporting V3 endpoints |
| [15](#15-quick-reference) | Quick reference |
| [16](#16-questions-for-the-api-owner) | Questions for the API owner |


## 0. Before you start

### 0.1 What IN-12915 does and does not tell you

IN-12915 is a genuine, closed delivery ticket for exactly the seven endpoints documented here. Its description is the authoritative endpoint list:

```
POST   /api/v3/reports/query
POST   /api/v3/reports/query/widget?widgetId={id}
POST  -/api/v3/reports/query/batch- → /api/v3/reports/batchQuery

GET    /api/v3/reports/engine
GET    /api/v3/reports/report?reportingEngineId={id}
GET    /api/v3/reports/custom-metric?reportingEngineId={id}&reportNames={name}
GET    /api/v3/reports/metadata?reportingEngineId={id}&reportNames={name}
```

Note the strike-through on the third line: the batch path was **changed during development** from `/reports/query/batch` to `/reports/batchQuery`. The OpenAPI document agrees with the corrected form.

| Role | Person |
|  --- | --- |
| Assignee (and prod verification) | Deepti Dagar |
| Reporter / creator | Ruchika Grover |
| Development (merge request 260960, branch `feature/IN-12915-master`) | Aman Joshi |
| QA scenarios | Lalith Kumar Sanklecha P |


> **⚠️ Conflict to resolve — the QA scenarios were written against superseded paths.** The QA scenario comment (2026-04-14) tests `POST /api/v3/reports/query?widgetId={id}` — with no `/widget` segment — and `POST /api/v3/reports/query/batch`. Both disagree with the ticket description and with `sprinklr-v3.yaml`, which define `/reports/query/widget` and `/reports/batchQuery`. The QA comment predates the path correction, so this guide follows the **description + OpenAPI document**. Confirm that QA re-ran against the corrected paths before the ticket was closed. See [§16, Q1](#16-questions-for-the-api-owner).


### 0.2 What is missing

| Gap | Effect on this guide |
|  --- | --- |
| **No endpoint specification documents supplied.** | No V3 request or response in this guide is transcribed from a verified V3 call. Shapes come from the OpenAPI document; illustrative values come from the V2 pages. |
| **The Postman collection on IN-12915 is unreachable.** Deepti Dagar attached `Reporting V3 API.postman_collection (...).json` (41,973 bytes, 2026-06-11). | This is almost certainly the single best source of verified V3 examples. If you can export it, every example below can be upgraded from *reconstructed* to *verified* in one pass. See [§16, Q2](#16-questions-for-the-api-owner). |
| **The V3 schemas carry no descriptions, no `required` arrays, and no enums.** | All field semantics in [§11](#11-shared-request-objects) are inherited from V2 and marked as such. |


### 0.3 Source links used

Both links supplied with this request were correct, and this guide uses them: IN-12915 for V3 context, and `dev.sprinklr.com/reporting` as the V2 index — from which all seven per-endpoint V2 pages were resolved.

## 1. Overview

The Reporting API returns the same analytics data that Sprinklr's reporting dashboards display. The OpenAPI document describes the `Reporting V3` tag as:

> Run analytics queries and retrieve reporting metadata. Supports single, widget, and batch queries, listing reporting engines and enabled report names, fetching report metadata and custom measurements, retrieving viral trend insights, returning compiled backend query strings, and Research Assistant queries.


### 1.1 The four concepts you need

Reporting is a four-level hierarchy. You must resolve each level before you can query.

| Concept | What it is | How to discover it |
|  --- | --- | --- |
| **Reporting engine** | The data domain — Listening, Paid, Inbound Analytics, Voice, and so on. Identified by an id such as `PLATFORM` or `INBOUND_MESSAGE`. | [`GET /reports/engine`](#4-get-reportsengine--list-reporting-engines) |
| **Report** | A dataset within an engine, such as `ACCOUNT_INSIGHTS` or `POST_INSIGHTS`. | [`GET /reports/report`](#5-get-reportsreport--list-report-names) |
| **Dimensions** (`groupBys`) | The attributes you slice by — channel, account, date. | [`GET /reports/metadata`](#6-get-reportsmetadata--fetch-metrics-and-dimensions) |
| **Measurements** (`projections`) | The numbers you aggregate — impressions, followers, handling time. | [`GET /reports/metadata`](#6-get-reportsmetadata--fetch-metrics-and-dimensions) and [`GET /reports/custom-metric`](#7-get-reportscustom-metric--fetch-custom-metrics) |


The V2 Blueprints article states the mapping to dashboard vocabulary plainly:

> Projections refer to the configured metrics… groupBys refer to the configured dimensions.


### 1.2 Which query endpoint should I use?

| If you… | Use | Why |
|  --- | --- | --- |
| Want data for a widget that already exists on a dashboard | [`POST /reports/query/widget`](#9-post-reportsquerywidget--custom-query-using-widget-id) | The widget already encodes the report, filters, dimensions and metrics. You supply only a time range and paging. |
| Want to build a query from scratch, or need filters the UI cannot express | [`POST /reports/query`](#8-post-reportsquery--custom-query) | Full control over `filters`, `groupBys`, `projections`, `sorts`. |
| Need several queries answered in one round trip, or your widget draws from more than one report type | [`POST /reports/batchQuery`](#10-post-reportsbatchquery--batch-query) | V2: *"If the configured metrics and dimensions in a widget are being pulled from multiple report types, batch query API is used."* |


### 1.3 The Generate API Payload workflow

The intended path to a working payload is **not** to hand-write one. Build the widget in the UI, then export its payload:

1. Create a dashboard and add a widget that shows the data you want.
2. Grant your user the **`Generate Widget API Payload`** role permission — Governance Console → All Settings → Workspace Roles (or Global Roles) → Create/Edit Role.
3. On the widget, open the **three-dot menu → Generate API v2 Payload**.
4. Choose **Copy Code** and paste the result into your request body.


Two constraints from the Blueprints article that surprise people:

- *"a separate API payload needs to be generated for every widget within the dashboard."*
- *"dashboard level filters will not be reflected in the payload that is generated."* — fold any dashboard-level filter into the widget, or add it manually to `filters`.


To use the exported payload with [`POST /reports/query/widget`](#9-post-reportsquerywidget--custom-query-using-widget-id), find `widgetId` **inside the `additional` object** of the generated payload.

> **⚠️ Conflict to resolve — the UI still generates a *v2* payload.** The menu item is labelled "Generate API **v2** Payload", and the V2 FAQ states the generated payload works for both v1 and v2. Nothing states whether it is valid against the v3 endpoints. The request schemas are field-for-field identical between V2 and V3 ([§13.3](#133-request-bodies-are-unchanged)), so it should port unmodified — but this is inference, not documented behaviour. See [§16, Q3](#16-questions-for-the-api-owner).


### 1.4 Prerequisites

- A registered Developer Portal application with an API key and secret. Best practice from the Blueprints article: register the app against a **service account**, not a personal user account, so the integration survives staff changes.
- An OAuth 2.0 access token. Validate it against the **"ME" endpoint** before debugging anything reporting-specific.
- Reporting **view permission** for the dashboards and workspaces you intend to query. A token whose user lacks workspace (client) access is the most common cause of a successful-but-empty response.
- The **`Generate Widget API Payload`** permission if you intend to export payloads from the UI.


### 1.5 Conventions used in this guide

- Base URL is written `{base}` and stands for `https://{host}/{env}` as resolved in [§2](#2-base-urls-and-environments).
- Credentials always appear as `{{accessToken}}` and `{{apiKey}}`. Never paste a real token into a shared document or ticket.
- All epoch timestamps are **milliseconds**.


## 2. Base URLs and environments

```
{base} = https://{host}/{env}
```

Full request path:

```
{base}/api/v3/reports/{operation}
```

| Segment | Value |
|  --- | --- |
| `{host}` | Region-specific API host, for example `api2.sprinklr.com` or `api3.sprinklr.com` |
| `{env}` | Environment/partner path segment issued with your Developer Portal application |


> **⚠️ Conflict to resolve — which host is correct.** All seven V2 reporting pages document `https://api3.sprinklr.com/{env}/…`. The Reporting Blueprints sample also uses `api3.sprinklr.com`, but the Help Center copy of the *same* sample uses `api2.sprinklr.com`. Host selection is region- and partner-specific and is issued to you with your application; do not copy a host out of documentation. See [§16, Q4](#16-questions-for-the-api-owner).


> **Specification gap.** `sprinklr-v3.yaml` declares paths relative to a server root and does not enumerate the production hosts or the `{env}` segment, so the base URL cannot be confirmed from the specification alone. This gap is present across all V3 entities, not only Reporting.


## 3. Authentication and common headers

All seven endpoints use the same headers. The V2 pages document four:

| Header | Value | Required | Notes |
|  --- | --- | --- | --- |
| `Authorization` | `Bearer {{accessToken}}` | Yes | OAuth 2.0 access token. |
| `Key` | `{{apiKey}}` | Yes | Your Developer Portal API key. The V2 pages show the literal placeholder value `api-key`; substitute your own key. |
| `Content-Type` | `application/json` | On `POST` | Required for the three query endpoints. |
| `Accept` | `application/json` | Recommended | Declares the acceptable response type. |


> **Specification gap.** The V3 document defines `401 Unauthorized` and `403 Forbidden` responses but does not declare a security scheme on these operations, so the specification does not state that `Key` is mandatory or how `Authorization` is formed. The header contract above is inherited from the V2 pages. See [§16, Q5](#16-questions-for-the-api-owner).


Header casing note: HTTP header names are case-insensitive, so `Key` and `key` are equivalent. The V2 pages use both spellings across different examples.

# Discovery endpoints

The four `GET` endpoints resolve the hierarchy described in [§1.1](#11-the-four-concepts-you-need). Call them in the order below the first time you integrate; cache the results afterwards, since engine and report catalogues change rarely.

## 4. `GET /reports/engine` — List reporting engines

**Summary.** List reporting engines visible to the current partner.
**operationId.** `ReportingApiV3_listReportingEngines`
**Response.** `200` with the standard `APIResponse` envelope.

This is the entry point. The `id` values it returns are what you pass as `reportingEngine` in a query body and as `reportingEngineId` to the other three discovery endpoints.

### 4.1 Parameters

None. The result is scoped implicitly to the partner and user behind the access token.

### 4.2 Example request

```bash
curl -X GET \
  '{base}/api/v3/reports/engine' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Accept: application/json'
```

### 4.3 Example response

```json
{
  "data": [
    { "id": "PLATFORM", "name": "Social Analytics" },
    { "id": "INBOUND_MESSAGE", "name": "Inbound Analytics" },
    { "id": "LISTENING", "name": "Listening" }
  ],
  "errors": []
}
```

### 4.4 Reporting engines

The following 38 engines were returned by the V2 endpoint. The set visible to *your* token depends on your partner's licensed products, so always call the endpoint rather than hard-coding this list.

| `id` | `name` |
|  --- | --- |
| `ADOPTION` | Adoption |
| `ADOPTION_ENGAGEMENT` | Adoption Engagement |
| `AUDIENCE` | Audience |
| `AUDIENCE_ACTIVITY` | Audience Activity |
| `AUDIENCE_INSIGHTS` | Audience Insights |
| `AUDIENCE_INSIGHT` | Social DMP |
| `AUDIENCE_LEAD` | AUDIENCE_LEAD |
| `BENCHMARKING` | Benchmarking |
| `SELF_SERVE_BENCHMARKING` | Benchmarking |
| `COMMUNITY` | Community |
| `CONSUMPTION` | Consumption Analytics |
| `CUSTOM_ENTITY` | Custom Entity |
| `GALLERY_AUDIENCE_ACTIVITY` | Gallery |
| `INBOUND_MESSAGE` | Inbound Analytics |
| `LISTENING` | Listening |
| `RDB_FIREHOSE` | Listening Explorer |
| `SELF_SERVE_RDB_FIREHOSE` | Listening Explorer |
| `LIVE_DATASET` | Live Dataset |
| `LYEARN_REPORTING_ENGINE` | LYEARN_REPORTING_ENGINE |
| `MONITORING_LOG` | Monitoring Log |
| `OUTBOUND_MESSAGE` | Outbound Message |
| `PAID` | Paid |
| `PLATFORM` | Social Analytics |
| `PLATFORM_HEALTH` | Platform Health |
| `REAL_TIME` | Real Time |
| `SELF_SERVE_MESSAGE` | Self Serve |
| `SELF_SERVE_LISTENING` | Self Serve |
| `SPR_TASK` | Task |
| `STORE_FRONT_AUDIENCE_ACTIVITY` | Store |
| `STORY_MESSAGE` | Story Analytics |
| `SELF_SERVE_STORY_MESSAGE` | Story Analytics |
| `UNIFIED_ANALYTICS_REPORTING_ENGINE` | Unified Analytics |
| `UNIVERSAL_COMMERCE_EVENT_ENGINE` | Universal Commerce |
| `UNIVERSAL_PROFILE` | Universal Profile |
| `VOICE` | Voice Analytics |
| `VOICE_SEGMENT_ACTIVATION_CONFIG` | VOICE_SEGMENT_ACTIVATION_CONFIG |
| `YOUTUBE_REPORTING_ENGINE` | YouTube Analytics |
| `wfm_reporting` | wfm_reporting |


Three things to note before you build a UI on top of this list:

- **`name` is not unique.** Benchmarking, Listening Explorer, Self Serve and Story Analytics each appear twice under different ids. Key your code on `id`, and if you present a picker, disambiguate the duplicates yourself.
- **`id` casing is not uniform.** `wfm_reporting` is lower-case while every other id is upper snake case. Do not normalise case when echoing an id back to the API.
- **Some entries expose an internal identifier as the display name** — `AUDIENCE_LEAD`, `LYEARN_REPORTING_ENGINE`, `VOICE_SEGMENT_ACTIVATION_CONFIG`. These are unlikely to be presentable to end users as-is.


> **⚠️ Conflict to resolve — which engines the query endpoints actually accept.** This endpoint advertises 38 engines, but the V2 Batch Query page states *"Supported engines include: PLATFORM, INBOUND_MESSAGE"*. Either the batch endpoint supports a narrower set than the engine list, or that sentence is an incomplete illustration. Nothing in the V3 specification resolves it. See [§16, Q6](#16-questions-for-the-api-owner).


> **Specification gap.** The `200` schema is the bare `APIResponse` envelope, whose `data` is typed `object`. The array-of-`{id, name}` shape above is inherited from V2 and is not encoded in V3. The QA scenario for this endpoint mentions *"pagination behavior if applicable"*, but no paging parameters are defined. See [§16, Q7](#16-questions-for-the-api-owner).


## 5. `GET /reports/report` — List report names

**Summary.** List the reports enabled for a reporting engine.
**Response.** `200` with the standard `APIResponse` envelope.

Given an engine id from [§4](#4-get-reportsengine--list-reporting-engines), this returns the reports available within it. The values feed the `report` field of a query body and the `reportNames` parameter of [§6](#6-get-reportsmetadata--fetch-metrics-and-dimensions) and [§7](#7-get-reportscustom-metric--fetch-custom-metrics).

### 5.1 Parameters

| Parameter | In | Required | Type | Description |
|  --- | --- | --- | --- | --- |
| `reportingEngineId` | query | See note | string | The reporting engine id, for example `PLATFORM`. Obtain it from [`GET /reports/engine`](#4-get-reportsengine--list-reporting-engines). |


> **Specification gap.** The IN-12915 description writes this endpoint as `GET /api/v3/reports/report?reportingEngineId={id}` and the V2 equivalent marks the engine id **Required**. The V3 specification entry for `/reports/report` does not declare the parameter in the same detailed form as its sibling endpoints. Treat `reportingEngineId` as **effectively required** — a report catalogue is meaningless without an engine — and confirm the behaviour when it is omitted. See [§16, Q8](#16-questions-for-the-api-owner).


### 5.2 Example request

```bash
curl -X GET \
  '{base}/api/v3/reports/report?reportingEngineId=PLATFORM' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Accept: application/json'
```

### 5.3 Example response

The response is an array of `{id, name}` objects, one per report. The `PLATFORM` engine returns several hundred; this is an abridged extract.

```json
{
  "data": [
    { "id": "ACCOUNT_INBOUND_VOLUME", "name": "ACCOUNT_INBOUND_VOLUME" },
    { "id": "ACCOUNT_INSIGHTS", "name": "ACCOUNT_INSIGHTS" },
    { "id": "AGENT_PERFORMANCE_AGGREGATED_REPORT", "name": "AGENT_PERFORMANCE_AGGREGATED_REPORT" },
    { "id": "ANALYTICS", "name": "ANALYTICS" },
    { "id": "FACEBOOK_HOURLY_INSIGHTS", "name": "FACEBOOK_HOURLY_INSIGHTS" },
    { "id": "POST_INSIGHTS", "name": "POST_INSIGHTS" },
    { "id": "CaseSLAReport", "name": "CaseSLAReport" }
  ],
  "errors": []
}
```

### 5.4 Response parameters

| Parameter | Type | Description |
|  --- | --- | --- |
| `data` | array | One object per report enabled for the engine. |
| `data[].id` | string | Report identifier. Pass this as `report` in a query body. |
| `data[].name` | string | Display name. In practice identical to `id` for most reports. |
| `errors` | array | Error objects, empty on success. |


Note that report ids are **not** uniformly upper snake case — the V2 response mixes `ACCOUNT_INSIGHTS` with `CaseSLAReport`, `CaseProcessingSLAReport` and `ComprehensiveCaseSLAReport`. Treat report ids as opaque, case-sensitive strings.

## 6. `GET /reports/metadata` — Fetch metrics and dimensions

**Summary.** Get report metadata for a reporting engine.
**operationId.** `ReportingApiV3_getMetadata`
**Response.** `200` with the standard `APIResponse` envelope.

This is the endpoint that tells you **what you are allowed to put in `projections` and `groupBys`**. For each requested report it returns the full catalogue of measurements (metrics), dimensions, and filter dimensions, with their field names, data types and display names.

### 6.1 Parameters

| Parameter | In | Required (V3 spec) | Required (V2) | Type | Description |
|  --- | --- | --- | --- | --- | --- |
| `reportingEngineId` | query | `false` | **Required** | string | Reporting engine id. |
| `reportNames` | query | `false` | **Required** | string | Optional comma-separated report names; **omit to return all reports**. |


> **⚠️ Conflict to resolve — required-ness of both parameters.** V3 marks both `required: false`, and the `reportNames` description explicitly supports omission (*"omit to return all reports"*). The V2 page marks both **Required**. Omitting `reportNames` on a large engine returns a very large payload — the metadata for a single report, `POST_INSIGHTS`, already runs to hundreds of lines. Confirm whether omission is genuinely supported in V3 and whether a cap applies. See [§16, Q9](#16-questions-for-the-api-owner).


### 6.2 Example request

```bash
curl -X GET \
  '{base}/api/v3/reports/metadata?reportingEngineId=PLATFORM&reportNames=POST_INSIGHTS' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Accept: application/json'
```

To request several reports, comma-separate them:

```
?reportingEngineId=PLATFORM&reportNames=POST_INSIGHTS,ACCOUNT_INSIGHTS
```

### 6.3 Example response

Heavily abridged — a real response for one report is several hundred lines.

```json
{
  "data": {
    "POST_INSIGHTS": {
      "recordType": "LIFETIME",
      "displayName": "POST_INSIGHTS",
      "dateFieldName": "date",
      "skipVisibilityFilters": false,
      "measurements": [
        {
          "type": "CUMULATIVE",
          "measurementGroups": ["FACEBOOK"],
          "name": "POST_FB_NEGATIVE_FEEDBACK_BY_TYPE_UNIQUE_HIDE_ALL_CLICKS",
          "fieldName": "POST_FB_NEGATIVE_FEEDBACK_BY_TYPE_UNIQUE_HIDE_ALL_CLICKS",
          "displayName": "Facebook Post Negative Feedback Unique All Hide",
          "isShared": false,
          "filterMeasurement": true,
          "hidden": false,
          "enabledForAiConcierge": false,
          "channelTypes": ["FACEBOOK"]
        }
      ],
      "dimensions": [
        {
          "type": "SCRIPT",
          "name": "postFeatures.ocr_gcv",
          "fieldName": "postFeatures.ocr_gcv",
          "displayName": "Creative Insight: All Text in Asset",
          "filterDimension": true,
          "multiValue": true,
          "sortable": false,
          "lookupSupported": true,
          "hidden": false
        }
      ],
      "oldCustomFieldDimensionName": "OUTBOUND_CUSTOM_PROPERTY_NAME"
    }
  },
  "errors": []
}
```

### 6.4 Response parameters

| Parameter | Type | Description |
|  --- | --- | --- |
| `data` | object | Keyed by report name — one entry per report you requested. |
| `data.{reportName}` | object | Metadata for that report. |
| `data.{reportName}.recordType` | string | Record type, for example `LIFETIME`. |
| `data.{reportName}.measurements` | array | Measurement (metric) objects. The `fieldName` of each is what you put in `projections[].measurementName`. |
| `data.{reportName}.dimensions` | array | Dimension objects. The `fieldName` of each is what you put in `groupBys[].dimensionName` and `filters[].dimensionName`. |
| `data.{reportName}.filterDimensions` | array | Dimension objects usable specifically for filtering. |
| `data.{reportName}.oldCustomFieldDimension` | object | Legacy custom-field dimension details. |
| `data.{reportName}.oldCustomFieldDimensionName` | string | Name of the legacy custom-field dimension. |
| `data.{reportName}.skipVisibilityFilters` | boolean | Whether visibility filters are bypassed for this report. |
| `data.{reportName}.displayName` | string | Display name of the report. |
| `data.{reportName}.dateFieldName` | string | The field used as the date field — a good default for `timeField` in a query body. |
| `errors` | array | Error objects, empty on success. |


Commonly useful fields on each measurement and dimension object:

| Field | Type | Meaning |
|  --- | --- | --- |
| `name` / `fieldName` | string | The identifier to use in a query. Use `fieldName`. |
| `displayName` | string | Human-readable label as shown in the UI. |
| `dataType` | string | For example `NUMERIC`, `TIME_DIFFERENCE`. |
| `type` | string | For example `CUMULATIVE`, `DELTA`, `SCRIPT`, `CUSTOM`. |
| `measurementGroups` | array | Groups the measurement belongs to, for example `COMMON`, `FACEBOOK`, `VK`. |
| `channelTypes` | array | Channels the measurement applies to. |
| `hidden` | boolean | Hidden measurements are generally not intended for direct use. |
| `filterMeasurement` / `filterDimension` | boolean | Whether the field may be used in `projectionFilters` / `filters`. |
| `multiValue` | boolean | Whether a dimension can hold multiple values per record. |
| `sortable` | boolean | Whether the field can be used in `sorts`. |
| `lookupSupported` | boolean | Whether the dimension's values can be resolved through a lookup. |
| `translatable` | boolean | Whether the dimension's values can be translated. |
| `dashboardGroups` | array | The dashboard groups the dimension is exposed in, for example `PARTNER_DASHBOARD`, `OVERALL_DASHBOARD`, `QM_DASHBOARDS`. |


> **Note — a misspelled key is part of the contract.** The V2 metadata response contains the literal key `showAsDashbaordFilter` (note the transposed letters in "Dashboard") inside a dimension's `additional` object. Misspelled keys in a live response cannot be silently corrected without breaking consumers. Do not "fix" this string in your parsing code. Confirm it is still spelled this way in V3 — see [§16, Q10](#16-questions-for-the-api-owner).


### 6.5 Resolving a custom property to its UI label

When a response contains a custom property field name you do not recognise, copy that field name from the request and pass it to the **fetch custom field using field name** API to retrieve the label shown in the UI.

## 7. `GET /reports/custom-metric` — Fetch custom metrics

**Summary.** Get custom measurements for a reporting engine.
**operationId.** `ReportingApiV3_getCustomMetrics`
**Response.** `200` with the standard `APIResponse` envelope.

Custom measurements are metrics your organisation has *defined itself* in Sprinklr — typically a formula over one or more standard measurements. [§6](#6-get-reportsmetadata--fetch-metrics-and-dimensions) returns the standard catalogue; this endpoint returns the customer-defined additions, together with the formula behind each.

### 7.1 Parameters

| Parameter | In | Required (V3 spec) | Required (V2) | Type | Description |
|  --- | --- | --- | --- | --- | --- |
| `reportingEngineId` | query | `false` | **Required** | string | Reporting engine id. |
| `reportNames` | query | `false` | **Required** | string | Optional comma-separated report names; **omit for all reports**. |


> **⚠️ Conflict to resolve — parameter name casing in the V2 example.** The V2 page documents the parameter as `reportNames` in its table but its own cURL example sends `?reportnames=ACCOUNT_INSIGHTS` (lower-case `n`). Query parameter names are case-sensitive in most server frameworks, so one of the two is wrong. The V3 specification declares `reportNames`, which is what this guide uses. See [§16, Q11](#16-questions-for-the-api-owner).


### 7.2 Example request

```bash
curl -X GET \
  '{base}/api/v3/reports/custom-metric?reportingEngineId=PLATFORM&reportNames=ACCOUNT_INSIGHTS' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Accept: application/json'
```

### 7.3 Example response

```json
{
  "data": {
    "ACCOUNT_INSIGHTS": [
      {
        "script": " (__formula__elem__453*1000)",
        "requiredProjections": [
          {
            "key": "__formula__elem__453",
            "measurement": "FOLLOWERS_COUNT",
            "aggregateFunction": "SUM"
          }
        ],
        "measurementGroups": ["COMMON"],
        "groupNames": ["Custom Measurement"],
        "isShared": false,
        "sharedKey": "EXAMPLE_METRIC_1",
        "filterMeasurement": true,
        "name": "EXAMPLE_METRIC_1",
        "fieldName": "EXAMPLE_METRIC_1",
        "displayName": "Followers per thousand",
        "lcName": "followers per thousand",
        "hidden": false,
        "enabledForAiConcierge": false,
        "dataType": "NUMERIC",
        "additional": {
          "measurement_type": "Custom Measurement",
          "clientId": 0000,
          "IS_PERMISSIBLE": "false",
          "definitionId": "000000000000000000000000"
        }
      },
      {
        "script": " (__formula__elem__1001+__formula__elem__1002)",
        "requiredProjections": [
          {
            "key": "__formula__elem__1001",
            "measurement": "FOLLOWERS_COUNT",
            "aggregateFunction": "SUM"
          },
          {
            "key": "__formula__elem__1002",
            "measurement": "FACEBOOK_PAGE_FANS",
            "aggregateFunction": "SUM"
          }
        ],
        "type": "CALCULATED",
        "measurementGroups": ["COMMON"],
        "groupNames": ["Custom Measurement"],
        "isShared": false,
        "sharedKey": "EXAMPLE_METRIC_2",
        "filterMeasurement": true,
        "name": "EXAMPLE_METRIC_2",
        "fieldName": "EXAMPLE_METRIC_2",
        "displayName": "Total audience",
        "lcName": "total audience",
        "filters": [],
        "hidden": false,
        "enabledForAiConcierge": false,
        "dataType": "NUMERIC",
        "additional": {
          "definitionId": "000000000000000000000001",
          "measurement_type": "Custom Measurement",
          "IS_PERMISSIBLE": "false",
          "clientId": 0000
        }
      }
    ]
  },
  "errors": []
}
```

*The metric names, display names, `sharedKey`, `definitionId` and `clientId` values above are illustrative placeholders and do not identify a real customer configuration.*

### 7.4 Response parameters

| Parameter | Type | Description |
|  --- | --- | --- |
| `data` | object | Container keyed by report name. |
| `data.{reportName}` | array | Custom measurement objects for that report. |
| `data.{reportName}[].script` | string | The formula evaluated to produce the metric, expressed over `__formula__elem__*` placeholders. |
| `data.{reportName}[].requiredProjections` | array | The standard measurements the formula depends on. Each has a `key` (matching a placeholder in `script`), a `measurement` name, and an `aggregateFunction`. |
| `data.{reportName}[].type` | string | Measurement type, for example `CALCULATED`, `DELTA`. |
| `data.{reportName}[].measurementGroups` | array | Groups the measurement belongs to. |
| `data.{reportName}[].groupNames` | array | Group display names, typically `Custom Measurement`. |
| `data.{reportName}[].isShared` | boolean | Whether the measurement is shared. |
| `data.{reportName}[].sharedKey` | string | Key for shared measurements. |
| `data.{reportName}[].filterMeasurement` | boolean | Whether the measurement can be used in `projectionFilters`. |
| `data.{reportName}[].name` / `fieldName` | string | Identifier to use in `projections[].measurementName`. |
| `data.{reportName}[].displayName` | string | Display label. |
| `data.{reportName}[].lcName` | string | Lower-cased display name. |
| `data.{reportName}[].shortName` | string | Abbreviated label. |
| `data.{reportName}[].hidden` | boolean | Whether the measurement is hidden in the UI. |
| `data.{reportName}[].enabledForAiConcierge` | boolean | Whether the measurement is exposed to AI Concierge. |
| `data.{reportName}[].dataType` | string | For example `NUMERIC`, `TIME_DIFFERENCE`. |
| `data.{reportName}[].filters` | array | Filters baked into the custom measurement definition. |
| `data.{reportName}[].additional` | object | Definition metadata — `definitionId`, `clientId`, `measurement_type`, `IS_PERMISSIBLE`. |
| `data.{reportName}[].channelTypes` | array | Channels the measurement applies to. |
| `data.{reportName}[].code` | string | Internal label code. |
| `errors` | array | Error objects, empty on success. |


To use a custom metric in a query, put its `fieldName` in `projections[].measurementName` exactly as returned — Sprinklr resolves the formula server-side. You do not need to reproduce `script` or `requiredProjections` yourself.

> **⚠️ Conflict to resolve — defects in the V2 reference page.** The V2 Fetch Custom Metrics page has three problems that a reader will hit: its example JSON is **malformed** (a stray `"code"` member appears after a closing brace); its response-schema table misspells the field as `enabledForAiConierge` while its own example uses the correctly spelled `enabledForAiConcierge`; and the table describes a `{GANALYTICS4_GENDER}` key that appears nowhere in its example. This guide documents the correctly spelled form and a generic `{reportName}` key. Confirm the true wire spelling. See [§16, Q11](#16-questions-for-the-api-owner).


# Query endpoints

## 8. `POST /reports/query` — Custom query

**Summary.** Execute a reporting query (plain `ReportRequest`).
**operationId.** `ReportingApiV3_query`
**Request body.** `ReportRequest`, required.
**Response.** `200` with the standard `APIResponse` envelope.

The general-purpose endpoint. You specify the engine, the report, the time range, and the dimensions, metrics, filters and sorts explicitly.

### 8.1 Parameters

No path or query parameters. Everything is in the body — see [§11.1](#111-reportrequest).

### 8.2 Example request

```bash
curl -X POST \
  '{base}/api/v3/reports/query' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "reportingEngine": "PLATFORM",
    "report": "ACCOUNT_INSIGHTS",
    "startTime": 1735689600000,
    "endTime": 1738368000000,
    "timeZone": "America/New_York",
    "page": 0,
    "pageSize": 100,
    "groupBys": [
      {
        "heading": "Channel",
        "dimensionName": "SOCIAL_NETWORK",
        "groupType": "FIELD"
      }
    ],
    "projections": [
      {
        "heading": "Followers",
        "measurementName": "FOLLOWERS_COUNT",
        "aggregateFunction": "SUM"
      }
    ],
    "filters": [
      {
        "dimensionName": "ACCOUNT_ID",
        "filterType": "IN",
        "values": ["000000000000000000000000"]
      }
    ],
    "sorts": [
      {
        "heading": "Followers",
        "order": "DESC"
      }
    ],
    "jsonResponse": true
  }'
```

### 8.3 Example response

With `jsonResponse` set to `true`, rows are returned as objects keyed by heading:

```json
{
  "data": {
    "data": [
      {
        "Channel": "FACEBOOK",
        "Followers": 128394
      },
      {
        "Channel": "TWITTER",
        "Followers": 94021
      }
    ],
    "hasMore": false
  },
  "errors": []
}
```

With `jsonResponse` set to `false`, the same result arrives in the compact columnar form — see [§12.2](#122-the-jsonresponse-toggle).

### 8.4 Response parameters

| Parameter | Type | Description |
|  --- | --- | --- |
| `data` | object | Result container. |
| `data.data` | array | Result rows. Each key corresponds to a `heading` you supplied in `groupBys` or `projections`. |
| `data.hasMore` | boolean | `true` if further rows are available beyond this page. See [§12.3](#123-pagination-and-hasmore). |
| `errors` | array | Error objects, empty on success. |


## 9. `POST /reports/query/widget` — Custom query using widget id

**Summary.** Execute a reporting query for a dashboard widget.
**operationId.** `ReportingApiV3_queryWidget`
**Request body.** `WidgetReportRequest`, required.
**Response.** `200` with the standard `APIResponse` envelope.

The widget already defines the report, engine, dimensions, metrics and filters, so the body carries only the time range, paging and formatting options. This is the lowest-effort way to pull dashboard data programmatically.

### 9.1 Parameters

| Parameter | In | Required (V3 spec) | Type | Description |
|  --- | --- | --- | --- | --- |
| `widgetId` | query | **`false`** | string | Widget id. |
| `replaceHeadingsWithLabel` | query | `false` | boolean | Forwarded to the underlying widget request. |


> **⚠️ Conflict to resolve — `widgetId` is marked optional.** The specification declares `required: false`, but the operation cannot execute without a widget to resolve, the IN-12915 description writes the path as `?widgetId={id}`, and V2 carried the id as a mandatory **path** parameter. This is very likely a specification defect. Until it is corrected, always send `widgetId`. See [§16, Q12](#16-questions-for-the-api-owner).


> **Specification gap — `replaceHeadingsWithLabel`.** This parameter exists nowhere except the specification line quoted above. It has no V2 equivalent, no ticket mention and no QA scenario. Its description — *"Forwarded to the underlying widget request"* — does not say what it does, what the default is, or what "label" it substitutes. Presumably it swaps machine headings for UI display labels. Do not depend on it until confirmed. See [§16, Q13](#16-questions-for-the-api-owner).


### 9.2 Finding the widget id

1. Open the dashboard containing the widget.
2. Widget three-dot menu → **Generate API v2 Payload** → **Copy Code**.
3. In the copied payload, locate `widgetId` **inside the `additional` object**.


### 9.3 Request body fields

`WidgetReportRequest` defines only eight fields.

| Field | Type | Required (V2 page) | Description |
|  --- | --- | --- | --- |
| `startTime` | integer (int64) | **Required** | Start of the time range, epoch **milliseconds**. |
| `endTime` | integer (int64) | **Required** | End of the time range, epoch **milliseconds**. |
| `page` | integer (int32) | **Required** | Zero-based page index. |
| `pageSize` | integer (int32) | Optional | Rows per page. Keep at or below **1000** — see the warning below. |
| `interval` | string | Optional | Bucket interval. **Only required when the widget uses `"groupType": "DATE_HISTOGRAM"`.** |
| `skipResolve` | boolean | **Required** | Default `false`. When `true`, suppresses resolution of referenced entities. |
| `jsonResponse` | boolean | **Required** | Default `false`. Controls response shape — see [§12.2](#122-the-jsonresponse-toggle). |
| `timeZone` | string | **Required** | IANA time zone, for example `America/New_York`. |


> **⚠️ Conflict to resolve — required fields.** The Required column above is taken from the V2 Custom Query Using Widget Id page. `WidgetReportRequest` in `sprinklr-v3.yaml` has **no `required` array at all**, so a client generated from the specification will treat all eight as optional and will compile requests that fail at runtime. See [§16, Q14](#16-questions-for-the-api-owner).


> **Page size warning.** From the V2 page: it is *"recommended to maintain maximum page size of 1000 as exceeding it might lead to server load and frequent 504 Gateway Timeout error"*. The Blueprints article adds that limits *"vary from partner to partner"* and that exceeding yours throws **400 Bad request**.


### 9.4 Example request

```bash
curl -X POST \
  '{base}/api/v3/reports/query/widget?widgetId=000000000000000000000000' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "startTime": 1735689600000,
    "endTime": 1738368000000,
    "timeZone": "America/New_York",
    "page": 0,
    "pageSize": 1000,
    "skipResolve": false,
    "jsonResponse": false
  }'
```

### 9.5 Example response

With `jsonResponse` set to `false`, the widget endpoint returns the columnar form:

```json
{
  "data": {
    "headings": ["Channel", "Followers"],
    "rows": [
      ["FACEBOOK", 128394],
      ["TWITTER", 94021]
    ]
  },
  "errors": []
}
```

### 9.6 Response parameters

| Parameter | Type | Description |
|  --- | --- | --- |
| `data` | object | Result container. |
| `data.headings` | array of strings | Column headings, in the order the widget defines them. |
| `data.rows` | array of arrays | Result rows. Each inner array is positionally aligned with `headings`. |
| `errors` | array | Error objects, empty on success. |


## 10. `POST /reports/batchQuery` — Batch query

**Summary.** Execute multiple reporting queries in one request.
**operationId.** `ReportingApiV3_batchQuery`
**Request body.** `BatchReportRequest`, required.
**Response.** `200` with the standard `APIResponse` envelope.

Use this when you need several results in one round trip, or when a single widget draws its metrics and dimensions from more than one report type. From the V2 page:

> If the configured metrics and dimensions in a widget are being pulled from multiple report types, batch query API is used.


### 10.1 Request body fields

| Field | Type | Required (V2 page) | Description |
|  --- | --- | --- | --- |
| `requests` | object | **Required** | A map of **arbitrary caller-chosen keys** to `ReportRequest` objects. The keys you send are echoed back in the response. |
| `collate` | boolean | Optional | Default **`false`**. Groups multiple queries together in the response. |


The V2 page adds a useful diagnostic: *"If 'Collate' is present in the UI payload, it implies that the query is a batch query."* — so when you export a payload from the UI and see `collate`, route it here rather than to [§8](#8-post-reportsquery--custom-query).

> **Specification gap — what `collate` actually changes.** V2 describes the effect only as *"grouping multiple queries together in the response"*, and V3 supplies no description at all. The response shape difference between `collate: true` and `collate: false` is not documented anywhere. See [§16, Q15](#16-questions-for-the-api-owner).


### 10.2 Example request

```bash
curl -X POST \
  '{base}/api/v3/reports/batchQuery' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "requests": {
      "req1": {
        "reportingEngine": "PLATFORM",
        "report": "ACCOUNT_INSIGHTS",
        "startTime": 1735689600000,
        "endTime": 1738368000000,
        "timeZone": "America/New_York",
        "page": 0,
        "pageSize": 100,
        "groupBys": [
          { "heading": "Channel", "dimensionName": "SOCIAL_NETWORK", "groupType": "FIELD" }
        ],
        "projections": [
          { "heading": "Followers", "measurementName": "FOLLOWERS_COUNT", "aggregateFunction": "SUM" }
        ],
        "filters": []
      },
      "req2": {
        "reportingEngine": "INBOUND_MESSAGE",
        "report": "INBOUND_CASE",
        "startTime": 1735689600000,
        "endTime": 1738368000000,
        "timeZone": "America/New_York",
        "page": 0,
        "pageSize": 100,
        "groupBys": [
          { "heading": "Channel", "dimensionName": "SOCIAL_NETWORK", "groupType": "FIELD" }
        ],
        "projections": [
          { "heading": "Volume", "measurementName": "INBOUND_COUNT", "aggregateFunction": "SUM" }
        ],
        "filters": []
      }
    },
    "collate": false
  }'
```

### 10.3 Example response

Results are returned in a `reports` map keyed by the same keys you sent.

```json
{
  "data": {
    "reports": {
      "req1": {
        "headings": ["Channel", "Followers"],
        "rows": [
          ["FACEBOOK", 128394],
          ["TWITTER", 94021]
        ]
      },
      "req2": {
        "headings": ["Channel", "Volume"],
        "rows": [
          ["FACEBOOK", 4821],
          ["TWITTER", 3107]
        ]
      }
    }
  },
  "errors": []
}
```

### 10.4 Response parameters

| Parameter | Type | Description |
|  --- | --- | --- |
| `data` | object | Result container. |
| `data.reports` | object | Map keyed by your request keys. |
| `data.reports.{key}` | object | Result for that sub-query, in the same `headings`/`rows` form as [§9.5](#95-example-response). |
| `errors` | array | Error objects, empty on success. |


> **⚠️ Conflict to resolve — atomicity versus per-query errors.** The QA scenario asserts that batch *"processes multiple queries atomically and returns individual results with proper error handling per query"*. Those two properties are in tension: if processing is atomic, one bad sub-query should fail the whole request; if errors are handled per query, it is not atomic. Which happens when `req1` succeeds and `req2` references an invalid dimension? Is the HTTP status still `200`? Does the failing key appear in `reports` with an error, or vanish? See [§16, Q16](#16-questions-for-the-api-owner).


> **Note — the V2 example is internally inconsistent.** The V2 Batch Query page sends keys `req1` and `req2` but shows a response containing `req3`. The keys are caller-chosen and echoed back, so `req3` is a documentation error, not a behaviour.


### 10.5 The `additional` object in batch payloads

The V2 page documents these sub-fields of `additional` on a batch sub-request. They are typically populated automatically by the UI payload generator.

| Sub-field | Purpose |
|  --- | --- |
| `translateResponse` | Whether the response should be translated. |
| `dashboardId` | Source dashboard identifier. |
| `engine` | Reporting engine for the sub-request. |
| `widgetId` | Source widget identifier. |
| `showTotal` | Whether to include a total row. |
| `Currency` | Currency for monetary measurements. |
| `chartType` | Chart type the widget renders. |
| `showRolloverTrends` | Whether rollover trend data is included. |
| `TABULAR` | Tabular rendering flag. |


> **⚠️ Conflict to resolve — `additional` is typed as a string map.** In `sprinklr-v3.yaml`, `ReportRequest.additional` is `object` with `additionalProperties: string`. But V2 examples pass booleans (`showTotal`, `translateResponse`, `SKIP_RESOLVER`) and numbers. A strictly generated client will reject those values. Either the type should be widened, or the values must be sent as quoted strings. See [§16, Q17](#16-questions-for-the-api-owner).


## 11. Shared request objects

The objects below are shared by [§8](#8-post-reportsquery--custom-query) and [§10](#10-post-reportsbatchquery--batch-query).

> **Specification gap — this entire section is inherited from V2.** Not one property of `ReportRequest`, `Filter_reporting`, `Group_reporting`, `Projection_reporting` or `Sort_reporting` carries a `description` in `sprinklr-v3.yaml`, none of the objects declares a `required` array, and no enum is declared anywhere. Every description, required marker and allowed value in [§11.1](#111-reportrequest) through [§11.7](#117-reference-enums) comes from the V2 documentation. If V3 has changed any of them, this guide will be wrong and nothing in the specification would reveal it. See [§16, Q18](#16-questions-for-the-api-owner).


### 11.1 `ReportRequest`

| Field | Type | Required (V2) | Description |
|  --- | --- | --- | --- |
| `reportingEngine` | string | **Required** | Engine id from [§4](#4-get-reportsengine--list-reporting-engines). |
| `report` | string | **Required** | Report id from [§5](#5-get-reportsreport--list-report-names). |
| `startTime` | integer (int64) | **Required** | Start of range, epoch milliseconds. |
| `endTime` | integer (int64) | **Required** | End of range, epoch milliseconds. |
| `timeZone` | string | **Required** | IANA time zone. |
| `page` | integer (int32) | **Required** | Zero-based page index. |
| `pageSize` | integer (int32) | **Required** | Rows per page. Keep at or below 1000. |
| `groupBys` | array of [`Group`](#113-group_reporting) | **Required** | Dimensions to slice by. |
| `projections` | array of [`Projection`](#114-projection_reporting) | **Required** | Metrics to aggregate. |
| `filters` | array of [`Filter`](#112-filter_reporting) | **Required** | Filters applied before aggregation. Send `[]` if you have none. |
| `jsonResponse` | boolean | **Required** | Response shape toggle — see [§12.2](#122-the-jsonresponse-toggle). |
| `sorts` | array of [`Sort`](#115-sort_reporting) | Optional | Result ordering. |
| `projectionDecorations` | array of strings | Optional | Derived columns — see [§11.7](#117-reference-enums). |
| `additional` | object | Optional | Free-form options — see [§11.6](#116-the-additional-object). |
| `timeField` | string | Not documented in V2 | The date field the range applies to. Default comes from the report's `dateFieldName` ([§6.4](#64-response-parameters)). |
| `projectionFilters` | array of [`Filter`](#112-filter_reporting) | Not documented in V2 | Filters applied to aggregated measurement values, after grouping. |
| `skipResolve` | boolean | Not documented in V2 | Suppresses entity resolution. Compare `SKIP_RESOLVER` in [§11.6](#116-the-additional-object). |
| `streamRequestInfo` | `ExternalStreamRequestInfo` | Not documented in V2 | Stream request context. |


> **⚠️ Conflict to resolve — eleven required fields, none marked required.** The V2 Custom Query page marks `reportingEngine`, `report`, `startTime`, `endTime`, `timeZone`, `pageSize`, `page`, `groupBys`, `projections`, `filters` and `jsonResponse` as **Required**. `ReportRequest` in V3 declares **no `required` array**, so generated clients and validators will accept a body missing all eleven. See [§16, Q18](#16-questions-for-the-api-owner).


> **Specification gap — four fields exist only in V3.** `timeField`, `projectionFilters`, `skipResolve` and `streamRequestInfo` appear in `ReportRequest` but on no V2 page. Their descriptions above are inferred from their names and from adjacent behaviour, and should be treated as unverified. In particular, the relationship between the top-level `skipResolve` field and the `additional.SKIP_RESOLVER` flag is unclear — see [§16, Q19](#16-questions-for-the-api-owner).


> **Specification gap — `ExternalStreamRequestInfo` is not fully resolvable.** The schema has `streamFields` (an array whose item type the specification records as *"Unresolved type (ExternalStreamField)"*), `name`, `childName` and `details`. The item type is not defined in `sprinklr-v3.yaml`, so `streamRequestInfo` cannot be populated from the specification alone. See [§16, Q19](#16-questions-for-the-api-owner).


### 11.2 `Filter_reporting`

| Field | Type | Description |
|  --- | --- | --- |
| `dimensionName` | string | Dimension to filter on. Use a `fieldName` from [§6](#6-get-reportsmetadata--fetch-metrics-and-dimensions). |
| `filterType` | string | Comparison operator — see [§11.7](#117-reference-enums). |
| `values` | array | Values to compare against. |
| `details` | object | Additional filter configuration. |


### 11.3 `Group_reporting`

| Field | Type | Description |
|  --- | --- | --- |
| `heading` | string | Column heading for this dimension in the response. This is the key you will read in the result rows. |
| `dimensionName` | string | Dimension to group by. |
| `groupType` | string | Grouping strategy — see [§11.7](#117-reference-enums). |
| `details` | object | Additional grouping configuration. |
| `namedFilters` | object | Map of name to an array of `Filter` objects, for named sub-groupings. |


### 11.4 `Projection_reporting`

| Field | Type | Description |
|  --- | --- | --- |
| `heading` | string | Column heading for this metric in the response. |
| `measurementName` | string | Measurement to aggregate. Use a `fieldName` from [§6](#6-get-reportsmetadata--fetch-metrics-and-dimensions) or [§7](#7-get-reportscustom-metric--fetch-custom-metrics). |
| `aggregateFunction` | string | Aggregation to apply — see [§11.7](#117-reference-enums). |
| `details` | object | Additional projection configuration. |


### 11.5 `Sort_reporting`

| Field | Type | Description |
|  --- | --- | --- |
| `heading` | string | The heading to sort by. Must match a `heading` you defined in `groupBys` or `projections`. |
| `order` | string | `ASC` or `DESC`. |


Note that sorting is keyed on **`heading`**, not on `dimensionName` or `measurementName` — so headings must be unique within a request if you intend to sort.

### 11.6 The `additional` object

| Key | Documented effect |
|  --- | --- |
| `SKIP_RESOLVER` | Set to `true` to suppress automatic resolution of `CASE` / `ASSOCIATED_CASE` descriptions. Useful when you want raw ids and faster responses. |


Batch sub-requests carry additional keys, listed in [§10.5](#105-the-additional-object-in-batch-payloads).

### 11.7 Reference enums

**None of these enumerations is declared in `sprinklr-v3.yaml`.** All five fields are plain `type: string` in V3. The values below are taken from the V2 documentation and are the only published list.

**`filterType`** — used in `filters[]` and `projectionFilters[]`:

| Value | Meaning |
|  --- | --- |
| `IN` | Value is in the supplied list. |
| `NIN` | Value is not in the supplied list. |
| `EQUALS` | Exact match. |
| `GT` | Greater than. |
| `GTE` | Greater than or equal to. |
| `LT` | Less than. |
| `LTE` | Less than or equal to. |
| `BETWEEN` | Within a range. |
| `STARTS_WITH` | String prefix match. |
| `CONTAINS` | Substring match. |
| `EXISTS` | Field is present. |
| `FILTER` | Composite / nested filter. |


**`groupType`** — used in `groupBys[]`:

| Value | Meaning |
|  --- | --- |
| `FIELD` | Group by the raw field value. |
| `DATE_HISTOGRAM` | Bucket by time interval. **Requires `interval` on widget queries.** |
| `TIME_OF_DAY` | Bucket by hour of day. |
| `DAY_OF_WEEK` | Bucket by day of week. |
| `MONTH_OF_YEAR` | Bucket by month. |


**`aggregateFunction`** — used in `projections[]`:

| Value | Meaning |
|  --- | --- |
| `SUM` | Total. |
| `AVG` | Mean. |
| `MIN` | Minimum. |
| `MAX` | Maximum. |
| `STATS` | Summary statistics. |


**`order`** — used in `sorts[]`: `ASC`, `DESC`.

**`projectionDecorations`** — derived columns added alongside a projection:

| Value | Meaning |
|  --- | --- |
| `CHANGE` | Absolute change versus the comparison period. |
| `PERCENTAGE_CHANGE` | Percentage change versus the comparison period. |
| `PERCENTAGE` | Share of total. |


## 12. Response format and status codes

### 12.1 The envelope

All seven endpoints return the standard `APIResponse` envelope:

| Field | Type | Description |
|  --- | --- | --- |
| `data` | object | The payload. Shape varies by endpoint and by `jsonResponse` — see below. |
| `errors` | array of `Error` | Error objects. Empty on success. |
| `metadata` | `ResponseMetadata` | Response metadata. |


> **Specification gap.** `APIResponse.data` is typed as a bare `type: object` with no sub-schema, so **none of the four response shapes documented in this guide is encoded in the specification**. Code generated from `sprinklr-v3.yaml` will hand you an untyped map. All response tables in this guide are inherited from V2. See [§16, Q20](#16-questions-for-the-api-owner).


### 12.2 The `jsonResponse` toggle

`jsonResponse` changes the *structure* of the result, not merely its formatting. This trips up almost everyone migrating.

| `jsonResponse` | Shape | Best for |
|  --- | --- | --- |
| `true` | `{"data": {"data": [ {heading: value, …} ], "hasMore": false}}` — an array of objects, each keyed by heading. | Readability; direct mapping to typed models. |
| `false` | `{"data": {"headings": [...], "rows": [[...]]}}` — a heading array plus positionally aligned row arrays. | Compactness; large result sets; direct load into a dataframe or CSV. |


Four distinct response shapes appear across the seven endpoints:

| Endpoint | Shape |
|  --- | --- |
| [`GET /reports/engine`](#4-get-reportsengine--list-reporting-engines), [`GET /reports/report`](#5-get-reportsreport--list-report-names) | `data` is an **array** of `{id, name}`. |
| [`GET /reports/metadata`](#6-get-reportsmetadata--fetch-metrics-and-dimensions), [`GET /reports/custom-metric`](#7-get-reportscustom-metric--fetch-custom-metrics) | `data` is an **object keyed by report name**. |
| [`POST /reports/query`](#8-post-reportsquery--custom-query), [`POST /reports/query/widget`](#9-post-reportsquerywidget--custom-query-using-widget-id) | `data.data` + `data.hasMore`, **or** `data.headings` + `data.rows`, per `jsonResponse`. |
| [`POST /reports/batchQuery`](#10-post-reportsbatchquery--batch-query) | `data.reports`, a **map keyed by your request keys**. |


> **⚠️ Conflict to resolve — does `jsonResponse` still behave this way in V3?** The toggle is documented only on the V2 pages, and the field carries no description in V3. Because it changes the response *structure*, a silent behaviour change would break every consumer. Confirm explicitly. Note also that `WidgetReportRequest` and `ReportRequest` both expose `jsonResponse`, but batch sub-requests set it per sub-request — it is not clear whether a batch honours it per key or applies one setting to the whole response. See [§16, Q21](#16-questions-for-the-api-owner).


### 12.3 Pagination and `hasMore`

The V2 FAQ gives the contract:

> Check for the 'hasMore' flag at the end of every response. If hasMore is true, it implies that more data rows are present; if false, it means the end of results.


Practical guidance from the Blueprints article, which matters for large exports:

- Keep `pageSize` at or below **1000**. Exceeding your partner's limit throws **400 Bad request**; large pages also cause **504 Gateway Timeout**.
- For large exports, **narrow the time window rather than incrementing `page`** — step `startTime`/`endTime` through 5-hour or 24-hour slices. This is markedly more reliable than deep paging.
- The S3 export ceiling is **100M messages**.


> **Specification gap — `hasMore` on the columnar shape.** `hasMore` is documented on the `jsonResponse: true` shape. Whether it also appears alongside `headings`/`rows`, and whether it appears per sub-query in a batch response, is not documented. See [§16, Q22](#16-questions-for-the-api-owner).


### 12.4 Status codes

Every one of the seven operations declares exactly these five:

| Code | Meaning | Typical cause |
|  --- | --- | --- |
| `200` | Success | — |
| `400` | Bad Request | Malformed body; unknown dimension or measurement; `pageSize` above your partner limit; invalid time range. |
| `401` | Unauthorized | Missing, expired or invalid access token. |
| `403` | Forbidden | Token valid but the user lacks reporting or workspace access. |
| `404` | Not Found | Unknown `widgetId`, engine id or report name. |


> **Specification gap — no `429` and no `500`.** Neither a rate-limit response nor a server-error response is declared on any Reporting V3 operation, yet the V2 documentation explicitly warns about **504 Gateway Timeout** under large page sizes. A client generated from this specification will have no branch for `429`, `500`, `502` or `504`. Rate limits, retry guidance and idempotency are undocumented for Reporting. See [§16, Q23](#16-questions-for-the-api-owner).


### 12.5 Troubleshooting an empty or short result

The V2 documentation identifies two dominant causes of a `200` response with less data than expected:

1. **The token's user lacks workspace (client) access.** Reporting honours user-level governance, so an under-permissioned token returns a successful but thin result rather than a `403`.
2. **`pageSize` is too low.** Check `hasMore` before concluding the dataset is complete.


> **⚠️ Conflict to resolve — X / Twitter resyndication.** The Reporting Blueprints page states that exporting X (Twitter) data requires use-case approval and shows the error `"apiStatus": "Removed fields because of resyndication policy"`, with a developer note saying approval is on hold. The newer Help Center article states flatly that *"X (formerly Twitter) resyndication is currently not supported."* These cannot both be current. Confirm which applies before promising X data to a customer. See [§16, Q24](#16-questions-for-the-api-owner).


## 13. V2 → V3 migration

### 13.1 Endpoint mapping

| Operation | V2 | V3 | Change |
|  --- | --- | --- | --- |
| Custom query | `POST /api/v2/reports/query` | `POST /api/v3/reports/query` | Version only. |
| Custom query using widget id | `POST /api/v2/reports/query/{widgetId}` | `POST /api/v3/reports/query/widget?widgetId={id}` | **Breaking.** Id moves from path to query **and** the path gains a `/widget` segment. |
| Batch query | `POST /api/v2/reports/batchQuery` | `POST /api/v3/reports/batchQuery` | Version only. |
| Fetch reporting engines | `GET /api/v2/reports/engines` | `GET /api/v3/reports/engine` | **Breaking.** Plural → **singular**. |
| Fetch report names | `GET /api/v2/reports/reports/{reportingEngineId}` | `GET /api/v3/reports/report?reportingEngineId={id}` | **Breaking.** Doubled `reports/reports` collapses to singular `report`; id moves from path to query. |
| Fetch custom metrics | `GET /api/v2/reports/customMetric/{reportingEngineId}?reportNames={name}` | `GET /api/v3/reports/custom-metric?reportingEngineId={id}&reportNames={name}` | **Breaking.** camelCase → **kebab-case**; id moves from path to query. |
| Fetch metrics and dimensions | `GET /api/v2/reports/metadata/{reportingEngineId}?reportNames={name}` | `GET /api/v3/reports/metadata?reportingEngineId={id}&reportNames={name}` | **Breaking.** Id moves from path to query. |


### 13.2 The pattern behind the changes

Five of the seven paths changed, and the changes are not arbitrary. The V3 programme applies a consistent rewrite rule across entities: **verbs are removed from paths, all identifiers move from the path into query parameters, and collection nouns are singularised.**

Applied to Reporting, that produces exactly what you see: `engines` → `engine`, `reports/reports` → `report`, and every `{reportingEngineId}` path segment becomes `?reportingEngineId=`. The `customMetric` → `custom-metric` change additionally normalises path segments to kebab-case.

The practical consequence for migration is that **you cannot migrate by string-replacing `v2` with `v3`.** Five of seven endpoints need their URLs rebuilt.

### 13.3 Request bodies are unchanged

This is the good news. `ReportRequest`, `WidgetReportRequest` and `BatchReportRequest` carry the same fields in V3 that the V2 pages document. No documented field was renamed, removed or retyped; V3 adds four fields to `ReportRequest` that V2 never documented (`timeField`, `projectionFilters`, `skipResolve`, `streamRequestInfo`). A payload that worked against `POST /api/v2/reports/query` should work unmodified against `POST /api/v3/reports/query`.

What changed is only what the *specification says* about those fields — V3 dropped the required markers, descriptions and enums that V2 documented in prose. The wire contract appears to be the same; the documentation of it is thinner.

### 13.4 What else to check

| Area | V2 | V3 | Action |
|  --- | --- | --- | --- |
| Response envelope | `{data, errors}` | `{data, errors, metadata}` | A `metadata` member is now declared. Ensure your parser tolerates it. |
| Response `data` shapes | Documented per endpoint | Typed `object`, undocumented | Keep your V2-era models; do not regenerate response types from the V3 specification. |
| Required fields | Eleven documented on `ReportRequest` | None declared | Keep your V2-era client-side validation. Do not rely on the V3 specification to catch a missing `reportingEngine`. |
| Enums | Five documented | None declared | Keep your V2-era enum constants ([§11.7](#117-reference-enums)). |
| Status codes | — | `200/400/401/403/404` declared | `429`, `500` and `504` are still possible but undeclared. Keep your existing retry and timeout handling. |
| Headers | `Authorization`, `Key`, `Content-Type`, `Accept` | Same | No change. |


### 13.5 Migration checklist

- [ ] Rebuild the URL for all four `GET` endpoints — move `reportingEngineId` from the path into the query string.
- [ ] Change `reports/engines` to `reports/engine` (singular).
- [ ] Change `reports/reports/{id}` to `reports/report?reportingEngineId={id}`.
- [ ] Change `reports/customMetric/{id}` to `reports/custom-metric?reportingEngineId={id}` (note the hyphen).
- [ ] Change `reports/query/{widgetId}` to `reports/query/widget?widgetId={id}`.
- [ ] Leave `reports/query` and `reports/batchQuery` bodies untouched — only the version segment changes.
- [ ] Confirm your `Authorization` and `Key` headers are unchanged (they are).
- [ ] Retain client-side validation of the eleven V2-required `ReportRequest` fields, since V3 no longer declares them.
- [ ] Retain your `filterType`, `groupType`, `aggregateFunction`, `order` and `projectionDecorations` constants — V3 does not publish them.
- [ ] Verify your `jsonResponse` handling still produces the shape you expect on both `true` and `false`.
- [ ] Confirm `hasMore` is still emitted, and that your paging loop terminates on it.
- [ ] Ensure your parser ignores the newly declared `metadata` envelope member.
- [ ] Keep retry handling for `429`/`500`/`504` even though V3 does not declare them.
- [ ] Re-test with a `pageSize` at your partner's actual limit, not the documented 1000.


## 14. Related Reporting V3 endpoints not covered above

These sit under the same `Reporting V3` tag but are outside the scope of IN-12915 and of this guide.

| Endpoint | Method | Summary |
|  --- | --- | --- |
| `/reports/fetchViralTrends` | POST | Fetch viral trend insights for a reporting query. Body: `ReportRequest`. operationId `ReportingApiV3_fetchReportingViralTrends`. |
| `/reports/log/query` | POST | Return compiled backend query strings for a batch of report requests. Body: `BatchReportRequest`. operationId `ReportingApiV3_logQuery`. Useful for debugging what the backend actually ran. |
| `/reports/query/researchAssistant` | POST | Research Assistant reporting query. Marked **hidden** in the specification. |


There is also a separate `Reporting Suite V3` tag covering `/reportingSuite/*`, which manages report suites rather than executing queries.

> **Specification gap — publication status.** `/reports/query/researchAssistant` is explicitly marked hidden, and `/reports/log/query` looks like an internal debugging affordance. Neither has a V2 counterpart page. Confirm whether either should be published on the developer portal. See [§16, Q25](#16-questions-for-the-api-owner).


## 15. Quick reference

| Operation | Method and path | Key parameters | Body |
|  --- | --- | --- | --- |
| List reporting engines | `GET /api/v3/reports/engine` | — | — |
| List report names | `GET /api/v3/reports/report` | `reportingEngineId` | — |
| Fetch metrics and dimensions | `GET /api/v3/reports/metadata` | `reportingEngineId`, `reportNames` | — |
| Fetch custom metrics | `GET /api/v3/reports/custom-metric` | `reportingEngineId`, `reportNames` | — |
| Custom query | `POST /api/v3/reports/query` | — | `ReportRequest` |
| Custom query using widget id | `POST /api/v3/reports/query/widget` | `widgetId`, `replaceHeadingsWithLabel` | `WidgetReportRequest` |
| Batch query | `POST /api/v3/reports/batchQuery` | — | `BatchReportRequest` |


**A first integration, end to end:**

```
GET  /api/v3/reports/engine                                        → pick an engine id
GET  /api/v3/reports/report?reportingEngineId=PLATFORM             → pick a report id
GET  /api/v3/reports/metadata?reportingEngineId=PLATFORM
                            &reportNames=ACCOUNT_INSIGHTS          → pick dimensions + measurements
POST /api/v3/reports/query                                         → run it
```

**Things that will bite you:**

- `engine` is singular; `report` is singular; `custom-metric` is hyphenated.
- `jsonResponse` changes the response *structure*, not just its style.
- Epoch timestamps are **milliseconds**.
- `pageSize` above 1000 causes `400` or `504`. Narrow the time window instead of deep paging.
- `sorts[].heading` refers to a heading you defined, not to a field name.
- Dashboard-level filters are **not** included in a generated widget payload.
- A `200` with thin data usually means the token's user lacks workspace access.


## 16. Questions for the API owner

Twenty-five items that cannot be resolved from the supplied sources. Suggested owners are drawn from IN-12915.

| # | Question | Why it blocks | Suggested owner |
|  --- | --- | --- | --- |
| 1 | The QA scenarios test `POST /reports/query?widgetId=` and `POST /reports/query/batch`, but the description and specification define `/reports/query/widget` and `/reports/batchQuery`. Was QA re-run against the corrected paths? | If not, the closed status rests on tests of paths that no longer exist. | Lalith Kumar Sanklecha P |
| 2 | Can the Postman collection attached to IN-12915 be exported and shared? | It is the only known source of verified V3 request/response examples. Every example in this guide is currently reconstructed. | Deepti Dagar |
| 3 | Is the UI's "Generate API **v2** Payload" output valid against the v3 endpoints? | It is the documented way to build a payload. If it needs adaptation for v3, the workflow in [§1.3](#13-the-generate-api-payload-workflow) is wrong. | Aman Joshi |
| 4 | Which host is correct for production — `api2.sprinklr.com` or `api3.sprinklr.com`? | Two copies of the same sample disagree. Developers cannot construct a working base URL. | Ruchika Grover |
| 5 | Should a security scheme be declared on the Reporting V3 operations? | The specification declares `401`/`403` but no security scheme, so `Key` and `Authorization` are undocumented in V3. | Aman Joshi |
| 6 | Do the query endpoints accept all 38 engines, or only `PLATFORM` and `INBOUND_MESSAGE` as the V2 batch page implies? | Determines whether an engine picker can be built from `GET /reports/engine`. | Ruchika Grover |
| 7 | Does `GET /reports/engine` paginate? The QA scenario mentions *"pagination behavior if applicable"* but no paging parameters exist. | A silently truncated engine list would be hard to detect. | Lalith Kumar Sanklecha P |
| 8 | Is `reportingEngineId` required on `GET /reports/report`, and what happens if it is omitted? | The description implies required; the specification does not mark it. | Aman Joshi |
| 9 | Is omitting `reportNames` genuinely supported on `/reports/metadata` and `/reports/custom-metric`, and is the response capped? | The V3 descriptions invite omission; V2 marks it required. Metadata for one report already runs to hundreds of lines. | Aman Joshi |
| 10 | Is the key `showAsDashbaordFilter` (transposed spelling) still emitted in V3 metadata responses? | Consumers must match the literal key. If V3 corrected it, that is a silent breaking change. | Aman Joshi |
| 11 | Is the custom-metric query parameter `reportNames` or `reportnames`, and is the response field `enabledForAiConcierge` or `enabledForAiConierge`? | The V2 page's table and its own example disagree on both. Query parameter names and JSON keys are case- and spelling-sensitive. | Aman Joshi |
| 12 | `widgetId` is declared `required: false` on `/reports/query/widget`. Is this a specification defect? | A generated client will omit it and fail at runtime. | Aman Joshi |
| 13 | What does `replaceHeadingsWithLabel` do, what is its default, and which label does it substitute? | Undocumented outside a single specification line. Cannot be used safely. | Aman Joshi |
| 14 | Should `WidgetReportRequest` declare its six V2-required fields as `required`? | Generated clients will compile requests missing `startTime`, `timeZone` and more. | Aman Joshi |
| 15 | What exactly does `collate` change in the batch response shape? | The only description is *"grouping multiple queries together"*. Consumers cannot code against it. | Aman Joshi |
| 16 | Is batch processing atomic or per-query? If `req2` fails, what is the HTTP status and does `req1`'s result still return? | The QA scenario asserts both properties, which are mutually exclusive. Error handling cannot be written. | Lalith Kumar Sanklecha P |
| 17 | `ReportRequest.additional` is typed `additionalProperties: string`, but V2 examples pass booleans and numbers. Should the type be widened? | Strict clients will reject `showTotal: true`. | Aman Joshi |
| 18 | Will V3 adopt V2's required markers, descriptions and enums for the reporting schemas? | Currently no reporting property has a description, no object declares `required`, and no enum is declared. V2 prose is the only semantic source. | Ruchika Grover |
| 19 | What is the relationship between top-level `skipResolve` and `additional.SKIP_RESOLVER`, and where is `ExternalStreamField` defined? | Two apparent controls for the same behaviour, and `streamRequestInfo` cannot be populated because its item type is unresolved. | Aman Joshi |
| 20 | Should `APIResponse.data` be given per-operation sub-schemas for Reporting? | `data` is a bare `object`, so no response shape in this guide is machine-checkable. | Ruchika Grover |
| 21 | Does `jsonResponse` still toggle response structure in V3, and how does it behave for batch sub-requests? | It changes structure, not formatting. A silent change breaks every consumer. | Aman Joshi |
| 22 | Is `hasMore` emitted on the `headings`/`rows` shape, and per sub-query in a batch response? | Paging loops terminate on it. | Aman Joshi |
| 23 | Should `429` and `500` be declared? What are the Reporting rate limits, and is any operation idempotent? | V2 warns about `504` under load, but no error beyond `404` is declared. Retry logic is unspecifiable. | Ruchika Grover |
| 24 | Is X (Twitter) resyndication approval-pending or not supported? | Blueprints and the Help Center article state opposite things. Affects what can be promised to customers. | Ruchika Grover |
| 25 | Should `/reports/log/query` and `/reports/query/researchAssistant` be published? The latter is marked hidden. | Determines the portal's endpoint list. | Ruchika Grover |


*All request and response examples in this guide are illustrative. They were reconstructed from `sprinklr-v3.yaml` and the V2 reference pages rather than captured from verified V3 calls, because no endpoint specification documents were supplied and the Postman collection on IN-12915 was not reachable. Identifiers, metric names, timestamps and counts are placeholders. Credentials appear only as `{{accessToken}}` and `{{apiKey}}` — never paste a real token into a document, ticket or shared collection. Verify every example against your own environment before relying on it.*