Paginating through lists using cursor pagination
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.
Paginating with the next link
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.
Node.js example with the next link
This example uses the third-party axios library.
const axios = require("axios").default;
// In production, store credentials in environment variables
const 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}`);
}
})();
Python example with the next link
This example uses the third-party Requests library.
import requests
import os
# In production, store credentials in environment variables
ZENDESK_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 data
response = 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 variables
const 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 requests
import os
# In production, store credentials in environment variables
ZENDESK_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 data
response = 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.