---
updatedAt: 2026-07-01T23:19:06.000Z
agentTools:
  projectIndex: https://developer.medallia.com/medallia-apis/llms.txt
---

# Overview

Medallia Experience Cloud is the fabric over which customer and employee experiences can be captured, organized, analyzed, and actioned.  Raw data can be extracted from Experience Cloud for archiving in data warehouses, or aggregated analytics can be run on the data and results pulled out and used. Medallia Query API is the engine powering these use cases.

## Query API Anatomy

Query API is based on [GraphQL](https://graphql.org).  We'll cover the basics needed to query MEC below, but recommend you familiarize yourself with GraphQL's principles to get the most out of this guide.

<Callout icon="📘" theme="info">
  ### Requirement

  Before using Query API, note that Experience Programs must be set up in the target instance. Experience Programs create a necessary referenceable schema for GraphQL fields such as feedback, invitations, fields, and _programs_.
</Callout>

### GraphQL Structure

A GraphQL query is a JSON document that consists of two parts: the query itself and a map of variables that are applied to the query.  The following is the basic structure:

```json Request Payload
{
  "query": "...(query as a string here)...",
  "variables": {
    ...(key/value pairs here)...
  }
}
```

The query is flattened as a string, making it difficult to read in this raw form.  As such, most GraphQL clients provide input text areas for each of the two parts seen here (query/variables) and format the query as a multiline input, as shown below:

```text Query
query {
  me {
    roles {
      id
      name
    }
  }
}
```

```json Variables
{
  "variable1": "value",
  "variable2": "anotherValue"
}
```

This will get flattened into the following:

```json Request Payload
{
  "query": "query {\n  me {\n    roles {\n...(omitted)...",
  "variables": {
    "variable1": "value",
    "variable2": "anotherValue"
  }
}
```

We show the GraphQL queries being used in the prettified format to make it easier to comprehend what’s going on; please translate to your GraphQL client of choice as needed.

### Authentication

Query API uses OAuth 2.0 for authentication.  For all the examples below, we use `${TOKEN}` to refer to the OAuth 2.0 access token granted.  See [Authentication](https://developer.medallia.com/medallia-apis/reference/authentication) to learn how to obtain your token.

The data access role associated with the OAuth 2.0 account must be granted the Query API capability.

### Rate Limits

Query API is subject to the following rate limits:

* 70 requests per second.
* 975,000 requests per 24-hour window.
* 3,000,000 cost-unit limit per query.

<Callout icon="📘" theme="info">
  ### Cost and limits

  Medallia limits the number of API calls per company to prevent unintended bugs in API client code or malicious code from taking down production systems. If you need higher limits, contact your Medallia representative to make the request.
</Callout>

<Callout icon="🚧" theme="warn">
  ### Usage restriction

  Do not use Query API in front-end implementations where data is requested upon website load, for example, requesting NPS score to display on the brand site. For this and similar use cases, we recommend using a back-end integration to store the desired value and only update periodically.
</Callout>

### GraphQL Objects

The query schema exposes many endpoints with a vast set of options on each.  Endpoints are typically either record-level or aggregate-level in nature.  The below are some of the most popular:

* [*invitations*](#invitations-and-feedback): a view that includes both open/pending and completed records, such as customer transactions, events, survey invitations, and survey responses.
* [*feedback*](#invitations-and-feedback): a view that includes only completed records, such as customer transactions, events, survey invitations, and survey responses.
* [*customers*](#customers-cx-profiles): details about customers, independent of any invitations or feedback<br />aggregate: calculations that aggregate multiple survey responses together into a single metric.
* [*fields*](#fields): metadata about the survey questions and other fields in Experience Cloud.

We’ll explore these in further detail in the sections below.

### Online Documentation

Query API exposes the GraphQL schema for use with tools that contain online documentation capabilities, such as <Anchor target="_blank" href="https://altairgraphql.dev/">Altair GraphQL Client.</Anchor>  (Medallia has no affiliation with the Altair project.)  As this documentation reflects the “as-built” conditions for your MEC instance, we strongly recommend using such a tool to help craft your queries.

### Data Types

GraphQL is strongly-typed.  For the most part, they are what you would expect: *Boolean*, *Int*, *String*, etc.  *ID* values should be encoded and treated as strings, but are called out especially for their semantic nature.

## Invitations and Feedback

The *invitations* and *feedback* objects are very similar in nature, the only difference being whether to filter for records that have feedback attached to the event/transaction.

The *invitations* object represents all data records, regardless of the [*e\_status*](https://docs.medallia.com/medallia-experience-cloud/feedback/advanced-feedback-management/fields/e-fields) value.  The *feedback* object represents data records where the [*e\_status*](https://docs.medallia.com/medallia-experience-cloud/feedback/advanced-feedback-management/fields/e-fields) value is *COMPLETED*.  The *feedback* object should be used preferentially, as queries on it are optimized.

The below example shows some of the most commonly-used options:

```text Query
query sample(
    $filter: Filter,
    $numPerPage: Int!,
    $orderBy: [RecordOrder],
    $pageCursor: ID,
    $status: ID!
) {
  # Use "invitations" or "feedback"
  invitations(
      after: $pageCursor,
      filter: $filter,
      first: $numPerPage,
      orderBy: $orderBy
  ) {
    nodes {
      id
      fieldData(fieldId: $status) {
        values
      }
    }
    totalCount
    pageInfo {
      endCursor
      hasNextPage
    }
  }
}
```

```json Variables
{
  "filter": {
    "and": [
      {
        "fieldIds": [
          "e_creationdate"
        ],
        "gte": "2021-01-01"
      }
    ]
  },
  "numPerPage": 100,
  "orderBy": [
    {
      "fieldId":
          "e_responsedate",
      "direction": "ASC"
    }
  ],
  "pageCursor": null,
  "status": "e_status"
}
```

```json Response
{
  "data": {
    "invitations": {
      "nodes": [
        {
          "id": "12259397",
          "fieldData": {
            "values": [
              "COMPLETED"
            ]
          }
        },
        {
          "id": "12384775",
          "fieldData": {
            "values": [
              "COMPLETION_PENDING"
            ]
          }
        },
        ...
      ],
      "totalCount": 289759,
      "pageInfo": {
        "endCursor": "100",
        "hasNextPage": true
      }
    }
  }
}
```

### Filters

Filters are built using a standard boolean structure.  At the outer level, an aggregate boolean operator — such as *and*, *or*, *not*, etc. — wraps the individual conditions and other sub-aggregate operators.

The following pseudo-EBNF structure describes the various options and how they relate to each other:

```text EBNF Syntax for Filters
FILTER         := { <OPERATOR> }

OPERATOR       := always: <BOOLEAN>
                | and: [<INNER_FILTER>, ...]
                | not: <INNER_FILTER>
                | or: [<INNER_FILTER>, ...]
                | ...

INNER_FILTER   := <FIELD_FILTER>
                | ...

FIELD_FILTER   := { fieldIds: [<STRING>, ...], <FIELD_OPERATOR> }

FIELD_OPERATOR := gt: <STRING>
                | gte: <STRING>
                | lt: <STRING>
                | lte: <STRING>
                | in: [<STRING>, ...]
                | isEmpty: <BOOLEAN>
                | hasNumericValue: <BOOLEAN>
                | ...
```

Using this, we create a multitude of filter conditions that are complex and nested, such as this:

```json Complex Filter Structure
{
  "or": [
    {
      "and": [
        {
          "not": {
            "fieldIds": ["e_status"],
            "in": ["0"]
          }
        },
        {
          "fieldIds": ["e_creationdate"],
          "lt": "2021-01-01"
        }
      ]
    },
    {
      "fieldIds": ["e_responsedate"],
      "gte": "2021-01-01"
    }
  ]
}
```

### Field Data

Field data is accessed through the *nodes* wrapper.  It consists of an ID that maps to the *a\_surveyid* value (the unique record identifier on the Experience Cloud side) and a set of field accessor functions that enable typed access to field data.

Generally, the *fieldData* accessor is what can be used for most operations:

```text Query Snippet
nodes {
  fieldData(fieldId: "e_some_field_id") {
    labels
    values
  }
  fieldData(fieldId: "e_some_other_field_id") {
    labels
    values
  }
}
```

This will map the field’s values to a JSON key that matches the *fieldId* specified.

Sometimes, the *fieldId* will be passed in as a variable, but you will need to map it to a constant identifier.  We can use an alternate version of the GraphQL syntax to add a custom label to the *fieldData* value:

```text Query Snippet
nodes {
  myField: fieldData(fieldId: "e_some_field_id") {
    labels
    values
  }
  myOtherField: fieldData(fieldId: "e_some_other_field_id") {
    labels
    values
  }
}
```

```json Response Snippet
...
"nodes": [
  {
    "myField": {
      "values": [
        "SURVEY_ENGINE_ONLY"
      ]
    },
    "myOtherField": {
      "values": [
        7
      ]
    }
  },
  ...
],
...
```

### Pagination

Access to all of Experience Cloud data is controlled through consumable pages of data.  The *pageInfo* wrapper exposes two attributes that are relevant:

```text Query Snippet
pageInfo {
  hasNextPage
  endCursor
}
```

The *hasNextPage* boolean indicates whether another page of data exists in the query.  The *endCursor* ID is the pointer reference to the next page of data.

When *hasNextPage* is true and *endCursor* is not empty, pass the *endCursor* value into the next query’s after parameter:

```text Query
query example($endCursorFromPreviousPage: ID) {
  # Use "invitations" or "feedback"
  feedback(after: $endCursorFromPreviousPage) {
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
```

The *after* parameter is often paired with the first parameter, which controls the number of results shown per page.

## Customers (CX Profiles)

Profiles data is exposed through the *customers* object.  [Profiles](https://www.medallia.com/platform/cx-profiles/) is an add-on to the base Medallia Experience Cloud subscription. Contact your Medallia representative for further details.

The below example shows some of the most commonly-used options:

```text Query
query sample(
    $filter: ContactFilter,
    $numPerPage: Int!,
    $orderBy: ContactOrder,
    $pageCursor: ID
) {
  customers(
      after: $pageCursor,
      filter: $filter,
      first: $numPerPage,
      orderBy: $orderBy
  ) {
    nodes {
      id
      email
      firstname
      lastname
      phone
    }
    pageInfo {
      endCursor
      hasNextPage
    }
  }
}
```

```json Variables
{
  "filter": {
    "and": [
      {
        "attributeKey":
            "first_name",
        "in": ["Chris"]
      }
    ]
  },
  "numPerPage": 100,
  "orderBy": {
    "attributeKey":
        "phone",
    "direction": "ASC"
  },
  "pageCursor": null
}
```

```json Response
{
  "data": {
    "customers": {
      "nodes": [
        {
          "id": "1234567890",
          "email": "chris@example.medallia.com",
          "firstname": "Chris",
          "lastname": "Smith",
          "phone": "(555) 867-5309"
        },
        ...
      ],
      "pageInfo": {
        "endCursor": "100",
        "hasNextPage": true
      }
    }
  }
}
```

Pagination is performed similarly to what is described in [Invitations and Feedback](#invitations-and-feedback).

### Customer Analytics

The *nodes* wrapper can perform aggregate analytics for each customer profile returned, for example, to obtain a customer’s lifetime NPS. Medallia exposes several of these aggregate functions, each of which has many options and capabilities.  The below shows the *aggregate* function, a basic building block that allows you to calculate metric data for each customer:

```text Query Snippet
nodes {
  id
  email
  aggregate(definition: {
    data: { source: FEEDBACK },
    metric: { average: { field: { id: "e_ltr" } } }
  })
}
```

```json Response Snippet
...
"nodes": [
  {
    "id": "1234567890",
    "email": "chris@example.medallia.com",
    "aggregate": 9
  },
  ...
],
...
```

As with the *fieldData* nodes from *invitations*/*feedback*, we can set an alias on the function to give it a unique and descriptive name:

```text Query Snippet
nodes {
  id
  email
  avgLtr: aggregate(definition: {
    data: { source: FEEDBACK },
    metric: { average: { field: { id: "e_ltr" } } }
  })
}
```

```json Response Snippet
...
"nodes": [
  {
    "id": "1234567890",
    "email": "chris@example.medallia.com",
    "avgLtr": 9
  },
  ...
],
...
```

See the [Query API](https://docs.medallia.com/medallia-experience-cloud/integration/apis/query-api) for details on other aggregate operations that can be performed and customer data that can be extracted.

## Aggregate Analytics

Medallia’s powerful aggregate analytics can be accessed via Query API’s aggregation functions:

* *aggregate*: single-number aggregation (such as overall NPS)
* *aggregateRank*: calculates the rank of one dimension/metric relative to all others
* *aggregateTable*: organizes aggregation results in a two-dimensional tabular form

Next, we will explore the first two through examples to show how to use these building blocks.

## Simple Aggregations

A simple *aggregate* operation can be used to get a sense of overall behavior and trends:

```text Query
query {
  avgLtr: aggregate(definition: {
    data: { source: FEEDBACK },
    metric: { average: { field: { id: "e_ltr" } } }
  })
}
```

```json Response
{
  "data": {
    "avgLtr": 8.395187118201767
  }
}
```

We can filter the dataset to different segments to perform comparisons:

```text Query
query {
  overall: aggregate(definition: {
    data: { source: FEEDBACK },
    metric: {
      average: { field: { id: "e_ltr" } }
    }
  })

  segmentA: aggregate(definition: {
    data: {
      source: FEEDBACK,
      filter: { fieldIds: "e_responsedate", lt: "2021-01-01" }
    },
    metric: {
      average: { field: { id: "e_ltr" } }
    }
  })

  segmentB: aggregate(definition: {
    data: {
      source: FEEDBACK,
      filter: { fieldIds: "e_responsedate", gte: "2021-01-01" }
    },
    metric: {
      average: { field: { id: "e_ltr" } }
    }
  })
}
```

```json Response
{
  "data": {
    "overall": 8.395187118201767,
    "segmentA": 9.05432838178133,
    "segmentB": 8.102866137591433
  }
}
```

### Ranked Aggregations

An *aggregateRank* operation is useful where the account has a limited scope (such as a call center agent being granted access to only her specific unit) and wants to rank that relative to some larger dataset.

The following example is made using an account with access to a single unit, looking to rank that unit across all units available in a data view with ID “27”:

```text Query
query {
  aggregateRank(definition: {
    fieldKey: "e_unitid",
    metric: { average: { field: { id: "e_ltr" } } },
    minimumSampleSize: 15,
    myData: { source: FEEDBACK },
    rankedData: { dataView: { id: "27" } }
  }) {
    rank
    totalElements
    metricValue
  }
}
```

```json Response
{
  "data": {
    "aggregateRank": {
      "rank": 263,
      "totalElements": 831,
      "metricValue": 8.658536585365853
    }
  }
}
```

Ranked aggregations like this are useful when trying to contextualize where your limited data view fits relative to the larger context’s data view.  If your account has full access to the data anyway, you may need to apply *myData* filters to achieve the same result.

## Metadata

### programs

The *programs* field allows a remote system to introspect details about available Experience Programs, used to filter on a specific program in a subsequent query.

```graphql Sample query
query experiencePrograms {
  programs(
    first: 3
  ){
    nodes {
      name
      id
      createdBy
      createdOn
      description
      status
      responsesCount
      type
    }
  }
}
```

```json Response
{
  "data": {
    "programs": {
      "nodes": [
        {
          "name": "Contact Center transactional agent interaction",
          "id": "6688cde1-3d7f-4486-b9ec-076c73d51364",
          "createdBy": "_medallia_system_test",
          "createdOn": "2021-07-23 04:57:02",
          "description": "Program: Contact Center transactional agent interaction. Created by Merlin Spell",
          "status": "ACTIVE",
          "responsesCount": 2447,
          "type": "ENTERPRISE"
        },
        {
          "name": "Retail Store e-receipt",
          "id": "9a3317f9-dc77-4430-bced-b35d976d1d31",
          "createdBy": "_medallia_system_test",
          "createdOn": "2021-07-23 04:56:07",
          "description": "Program: Retail Store e-receipt. Created by Merlin Spell",
          "status": "ACTIVE",
          "responsesCount": 73610,
          "type": "ENTERPRISE"
        },
        {
          "name": "Wealth Management Client Onboarding",
          "id": "abbb51a8-7792-4c41-b731-c873b492dd72",
          "createdBy": "_medallia_system_test",
          "createdOn": "2021-07-23 04:56:56",
          "description": "Program: Wealth Management Client Onboarding. Created by Merlin Spell",
          "status": "ACTIVE",
          "responsesCount": 890,
          "type": "ENTERPRISE"
        }
      ]
    }
  },
  "errors": null,
  "_links": null,
  "_allowed": [
    "POST"
  ]
}
```

The *programs* field also allows a remote system to introspect the associated data schema for Experience Programs.

```graphql Sample query
query experiencePrograms {
  programs(
    first: 3
  	   ){
    nodes {
      id
      name
      responsesCount
      type
      fields{
        nodes{
          id
          name
          dataType
        }
      }
    }
  }
}
```

```json Response
{
  "data": {
    "programs": {
      "nodes": [
        {
          "id": "6688cde1-3d7f-4486-b9ec-076c73d51364",
          "name": "Contact Center transactional agent interaction",
          "responsesCount": 2447,
          "type": "ENTERPRISE",
          "fields": {
            "nodes": [
              {
                "id": "q_bp_cc_ltr_scale",
                "name": "Likelihood to Recommend Contact Center",
                "dataType": "ENUM"
              },
              {
                "id": "e_email",
                "name": "Email",
                "dataType": "EMAIL"
              },
              [...]
              {
                "id": "e_bp_uniquerecordid_txt",
                "name": "Unique Record Id",
                "dataType": "STRING"
              }
            ]
          }
        },
        {
          "id": "9a3317f9-dc77-4430-bced-b35d976d1d31",
          "name": "Retail Store e-receipt",
          "responsesCount": 73610,
          "type": "ENTERPRISE",
          "fields": {
            "nodes": [
              {
                "id": "q_bp_store_ltr_scale",
                "name": "Likelihood to Recommend Store",
                "dataType": "ENUM"
              },
              {
                "id": "e_firstname",
                "name": "First name",
                "dataType": "STRING"
              },
              [...]
              {
                "id": "e_bp_uniquerecordid_txt",
                "name": "Unique Record Id",
                "dataType": "STRING"
              }
            ]
          }
        },
        {
          "id": "abbb51a8-7792-4c41-b731-c873b492dd72",
          "name": "Wealth Management Client Onboarding",
          "responsesCount": 890,
          "type": "ENTERPRISE",
          "fields": {
            "nodes": [
              {
                "id": "q_bp_wm_ltr_scale",
                "name": "Likelihood to Recommend WM",
                "dataType": "ENUM"
              },
              {
                "id": "e_phone",
                "name": "Phone",
                "dataType": "STRING"
              },
              [...]
              {
                "id": "e_bp_uniquerecordid_txt",
                "name": "Unique Record Id",
                "dataType": "STRING"
              }
            ]
          }
        }
      ]
    }
  },
  "errors": null,
  "_links": null,
  "_allowed": [
    "POST"
  ]
}
```

<br />

#### Filters and pagination for schemas

Use a filter like the one shown below to get all fields for a specific program. By defining the parameter *first* on the internal *fields* operation — and not on the *programs* — you can paginate through the data fields of the program.

```graphql Query
query experienceProgramSchema {
  programs(
     ids: "51c1a508-cf61-4be7-901c-87f3989ab7e1"
  ) {
    nodes {
      name
      id
      fields(
        first: 300
        after: null,
      ) {
        nodes {
          id
          name
          dataType
        }
        pageInfo {
          endCursor
          hasNextPage
        }
        totalCount
      }
    }
  }
}

```

The *endCursor* returned in the previous response should be added as part of the pagination of the attributes in the next query. For example, if the response contains the following pagination information:

```json Pagination
[...]
"pageInfo": {
        "hasNextPage": true,
        "endCursor": "xWSJSCIGh9+8WBydu4cOW5xdK9Q"
      }
[...]
```

The *programs* query to get the next set of data should look like this:

```graphql Query
query experienceProgramSchema {
  programs(
     ids: "51c1a508-cf61-4be7-901c-87f3989ab7e1"
  ) {
    nodes {
      name
      id
      fields(
        first: 300
        after: "xWSJSCIGh9+8WBydu4cOW5xdK9Q",
      ) {
        nodes {
          id
          name
          dataType
        }
        pageInfo {
          endCursor
          hasNextPage
        }
        totalCount
      }
    }
  }
}
```

### Fields

The *fields* field allows a remote system to introspect details about [fields](https://docs.medallia.com/?resourceId=mec-reporting-fields), used to store data on a data record.

```graphql Sample query
query {
  fields {
    nodes {
      id
      name
      description
      dataType
      multivalued
    }
    pageInfo {
      hasNextPage
      endCursor
    }
    totalCount
  }
}
```

```json Sample response
{
  "data": {
    "fields": {
      "nodes": [
        {
          "id": "a_surveyid",
          "name": "Survey ID",
          "description": null,
          "dataType": "INT",
          "multivalued": false
        },
        ...
      ],
      "pageInfo": {
        "hasNextPage": true,
        "endCursor": "MzA="
      },
      "totalCount": 2290
    }
  }
}
```

The *ids* filter allows us to query a specific field:

```graphql Sample query
query {
  fields(ids: ["e_unitid"]) {
    nodes {
      id
      name
      description
      dataType
      multivalued
    }
  }
}
```

```json Sample response
Response
{
  "data": {
    "fields": {
      "nodes": [
        {
          "id": "e_unitid",
          "name": "Unit",
          "description": null,
          "dataType": "UNIT",
          "multivalued": false
        }
      ]
    }
  }
}
```

Other filters, such as for *dataTypes* or field ID search (*q*), are also available; see the [product documentation](https://docs.medallia.com/?resourceId=mec-metadata-queries) for details.

<HTMLBlock>{`
<div class="manual-whats-next">
  <hr>

  <div class="nextSteps">
    <h3>What's Next</h3>
    <p></p>
    <table>
      <tbody>
        <tr><td><a href="/medallia-apis/reference-link/query"><i class="fa fa-chevron-right"></i> Query API Specification</a></td></tr>
        <tr><td><a href="/medallia-apis/docs/extract-survey-responses-for-offline-analysis-data-warehousing"><i class="fa fa-chevron-right"></i> Extract Survey Responses And Analytics</a></td></tr>
        <tr><td><a href="https://github.com/medallia/query-api-data-extract"><i class="fa fa-chevron-right"></i> Query API Reference Implementation</a></td></tr>
      </tbody>
    </table>
  </div>
</div>
`}</HTMLBlock>

<br />