The Zendesk Ticketing, Voice, and Help Center APIs have several endpoints that return lists of items, such as tickets, users, or articles. For performance reasons, the API does not return large record sets all at once. It breaks up the results into smaller subsets and returns them in pages. The number of items per page varies by endpoint. For example, the tickets and users endpoints return 100 items per page while the articles endpoint returns 30 items per page.

This article explains how to paginate through the lists using cursor pagination. Cursor pagination is already supported by most resources, such as tickets and users. It is progressively being introduced to all resources. See the API documentation for the specific resource.

Zendesk recommends using cursor pagination instead of offset pagination where possible. Cursor pagination provides greatly improved performance when retrieving very large record sets. See Comparing cursor pagination and offset pagination for a comparison of the different pagination methods supported by the Zendesk APIs.

Disclaimer: Zendesk provides this article for instructional purposes only. Zendesk does not support or guarantee the code. Zendesk also can't provide support for third-party technologies such as Python and Ruby.

Enabling cursor pagination

To use cursor pagination instead of offset pagination, include a page[size] parameter in the request's path. This parameter specifies the number of items to return per page. Most endpoints limit this to a maximum of 100. See the API documentation for the specific resource.

If you don't specify a page[size] path parameter, the request reverts to offset pagination.

The HTTP response includes metadata to use to request the next page. For example, a request to the tickets endpoint with the URL https://example.zendesk.com/api/v2/tickets.json?page[size]=100 returns a response with the following format:

"tickets": [ ... ],"meta": {    "has_more": true,    "after_cursor": "xxx",    "before_cursor": "yyy"},"links": {    "next": "https://example.zendesk.com/api/v2/tickets.json?page[size]=100&page[after]=xxx",    "prev": "https://example.zendesk.com/api/v2/tickets.json?page[size]=100&page[before]=yyy"}

To request the next page, you can use the next link or the after_cursor cursor. See Paginating with the next link and Paginating with the after cursor.

When to stop paginating

Keep requesting each next page until the has_more property nested in the meta object is false. This indicates there are no further records and you should stop paginating.

After you reach the end of the records, you can save the value of next or after_cursor to retrieve new records in the future.

Occasionally, you may get an empty record set, where after_cursor, before_cursor, next and prev are null, even though has_more was true in the previous page. This occurs when the final record returned by the previous page is the final record in the entire record set. In this case, save the previous value of after_cursor for future use.

You can use the URL specified by the next property of the links object to retrieve the next page of results.

"links": {    "next": "https://example.zendesk.com/api/v2/tickets?page%5Bafter%5D=aQAAAAAAAAAAZGYzylUAAAAAaYBxUAwAAAAA&page%5Bsize%5D=20"},

The examples that follow make requests to the URL specified by the next property.

Note: Certain resources don't have a next property in their responses. In that case, you can use the after_cursor property to retrieve the next page of results. See Paginating with the after cursor.

This example uses the third-party axios library.

const axios = require("axios").default;
// In production, store credentials in environment variablesconst ZENDESK_SUBDOMAIN = "YOUR_ZENDESK_SUBDOMAIN";const ZENDESK_USER_EMAIL = "YOUR_ZENDESK_EMAIL_ADDRESS"const ZENDESK_API_TOKEN = "YOUR_ZENDESK_API_TOKEN";
let url = `https://${ZENDESK_SUBDOMAIN}.zendesk.com/api/v2/users.json`;const auth = Buffer.from(`${ZENDESK_USER_EMAIL}/token:${ZENDESK_API_TOKEN}`).toString('base64');
const headers = {  Authorization: `Basic ${auth}`};let params = { "page[size]": 10 };
(async () => {  try {    do {      const response = await axios.get(url, { headers: headers, params: params });      const data = response.data;        // do something with the data      for (const user of data.users) {        console.log(user.name);      }        if (data.meta.has_more) {        url = data.links.next;      } else {        url = null;      }    } while (url);  } catch (error) {    console.error(`Error: ${error}`);  }})();

This example uses the third-party Requests library.

import requestsimport os
# In production, store credentials in environment variablesZENDESK_SUBDOMAIN = 'YOUR_ZENDESK_SUBDOMAIN'ZENDESK_USER_EMAIL = 'YOUR_ZENDESK_EMAIL_ADDRESS'ZENDESK_API_TOKEN = os.getenv('YOUR_ZENDESK_API_TOKEN')
url = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com/api/v2/users.json"
# Construct the Basic Auth string using the API token.# The token should be coupled with '/token' postfix and the user email.auth = f'{ZENDESK_USER_EMAIL}/token', ZENDESK_API_TOKEN
params = {'page[size]': 10}
# Fetch the initial page of dataresponse = requests.get(url, params=params, auth=auth)
while url:      if response.status_code == 200:        data = response.json()
        # Do something with the data, such as print user names        for user in data['users']:            print(user['name'])
        # Check for another page and get it; if not, exit the loop        if data['meta']['has_more']:            url = data['links']['next']            response = requests.get(url, auth=auth)  # Get subsequent pages        else:            url = ''    else:        print(f"Failed to retrieve data, status code: {response.status_code}")        break

Note that the data['links']['next'] link carries over any url parameters from the initial request.

Paginating with the after cursor

To request the next page, copy the after_cursor value from the response to the page[after] path parameter of the next request.

"meta": {    "has_more": true,    "after_cursor": "aQAAAAAAAAAAZGYzylUAAAAAaYBxUAwAAAAA"},

The example that follows copies the after_cursor value from the response to the page[after] path parameter of the next request.

Note: Certain resources limit how long the value of after_cursor is valid for. See the API documentation for the specific resource.

Node.js example with the after cursor

const axios = require("axios")
// In production, store credentials in environment variablesconst ZENDESK_SUBDOMAIN = "YOUR_ZENDESK_SUBDOMAIN"const ZENDESK_USER_EMAIL = "YOUR_ZENDESK_EMAIL_ADDRESS"const ZENDESK_API_TOKEN = "YOUR_ZENDESK_API_TOKEN"
const url = `https://${ZENDESK_SUBDOMAIN}.zendesk.com/api/v2/users.json`;const auth = Buffer.from(`${ZENDESK_USER_EMAIL}/token:${ZENDESK_API_TOKEN}`).toString('base64');
const config = {  headers: {    Authorization: `Basic ${auth}`  },  params: {    'page[size]': 10  }};
(async () => {  try {    let response = await axios.get(url, config);        while (url) {        const data = response.data;
      // Process the data      data.users.forEach(user => {        console.log(user.name);      });
      // Check for another page and get it; if not, exit the loop      if (data.meta.has_more && data.links.next) {        url = data.links.next;        response = await axios.get(url, config);      } else {        url = null;      }    }  } catch (error) {    console.error(`An error occurred: ${error.response ? error.response.status : error.message}`);  }})();

Python example with the after cursor

import requestsimport os
# In production, store credentials in environment variablesZENDESK_SUBDOMAIN = 'YOUR_ZENDESK_SUBDOMAIN'ZENDESK_USER_EMAIL = 'YOUR_ZENDESK_EMAIL_ADDRESS'ZENDESK_API_TOKEN = os.getenv('YOUR_ZENDESK_API_TOKEN')
url = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com/api/v2/users.json"
# Construct the Basic Auth string using the API token.# The token should be coupled with '/token' postfix and the user email.auth = f'{ZENDESK_USER_EMAIL}/token', ZENDESK_API_TOKEN
params = {'page[size]': 10}
# Fetch the initial page of dataresponse = requests.get(url, params=params, auth=auth)
while url:        response = requests.get(url, params=params, auth=auth)    data = response.json()
    # do something with the data    for user in data['users']:        print(user['name'])
    # check for another page and get it; if not, exit the loop    if data['meta']['has_more']:        params['page[after]'] = data['meta']['after_cursor']    else:        url = ''```
## Filtering results
Certain resources allow filtering the record set by certain attributes. For example, users may be filtered by roles. See the API documentation for the specific resource.
## Sorting results
Certain resources allow sorting the record set by certain attributes. For example, tickets may be sorted by their creation date. See the API documentation for the specific resource.
## Limitations
It is not possible to jump to an arbitrary page number or location in the record set when using cursor pagination.
The total number of records in the set is not provided when using cursor pagination. Certain resources provide a different endpoint to retrieve this value. For example, the total number of users can be retrieved with the URL `https://example.zendesk.com/api/v2/users/count.json`. See the API documentation for the specific resource.
Paginated data may be inaccurate due to the addition, removal, or modification of records while you're paginating through the list. One way to reduce pagination inaccuracies -- though not eliminate them altogether -- is to sort the records by an attribute that cannot be modified, such as the id or the creation date, if this is supported by the resource.