REST Admin API pagination
The SHOPLINE REST Admin API uses cursor-based pagination to paginate query results. All APIs that support pagination adhere to a unified schema for pagination control (query parameters and response headers) and support the following two modes:
since_idmode: Used for initial requests (the first page), wheresince_idspecifies the starting point for data retrieval.page_infomode: Used for subsequent requests (non-first pages), wherepage_infois used to retrieve the next or previous page of data.
When responding to a paginated request, the REST Admin API returns a link field in the HTTP response header containing the full request URLs for the currently available previous page (rel="previous") or next page (rel="next"). To request the next or previous page, parse and issue a request directly to the full URL provided in the link header.
These pagination rules apply across all versions of the SHOPLINE REST Admin API.
Common query parameters
When making paginated queries, you can use the following common query parameters to control page navigation, page size, and returned fields:
page_info: A unique identifier for a specific page of results. It must be extracted from thelinkHTTP response header.limit: The maximum number of records to return per page. Default value:50. A maximum of 1,000 records is supported.fields: A list of fields to be returned, with multiple fields separated by commas. By default, all fields of the target resource are returned.
When page_info is present in the request URL, the system automatically inherits the filtering context from the initial request, and all filtering parameters except limit and fields will be ignored.
How to paginate results
To fetch a list of products from a SHOPLINE store with 10 items per page and navigate to the second page, follow these steps:
Step 1: Request the first page
Call the Get products API with limit=10 in the query parameters:
GET https://{handle}.myshopline.com/admin/openapi/{version}/products/products.json?limit=10
Step 2: Obtain the link response header
Check the link field in the HTTP response header.
If the response contains only the request URL for the next page, a sample response is as follows:
link: <https://{handle}.myshopline.com/admin/openapi/{version}/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9>; rel="next"
If the response contains the request URLs for both the previous page and the next page, a sample response is as follows:
link: <https://{handle}.myshopline.com/admin/openapi/{version}/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9>; rel="next", <https://{handle}.myshopline.com/admin/openapi/{version}/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9>; rel="previous"
If the link field is omitted in the response header, you have reached the last page of results and do not need to paginate further.
The rel attribute in the link field indicates the page relationship of the corresponding URL:
rel="next": The request URL for the next pagerel="previous": The request URL for the previous page
Step 3: Extract the URL and request the second page
Given the following link field in the response header:
link: <https://{handle}.myshopline.com/admin/openapi/{version}/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9>; rel="next", <https://{handle}.myshopline.com/admin/openapi/{version}/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9>; rel="previous"
Extract the full next page URL enclosed in angle brackets <> corresponding to rel="next":
https://{handle}.myshopline.com/admin/openapi/{version}/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9
Send a GET request directly to this extracted URL to retrieve the second page.
In Java, you can use the regular expression <([^>]+)>;\s*rel="([^"]+)" to quickly match and extract the target URL containing page_info.
Detailed parameter descriptions
since_id mode
The following table describes the query parameters used in since_id mode:
| Parameter | Type | Required | Description |
|---|---|---|---|
since_id | String | No | The starting resource ID, such as a product ID or order ID. Records with IDs greater than this value will be returned. Default value: 0 |
limit | Long | No | The number of records to return per page. The default value depends on the API schema. |
The following table describes the link header:
| Parameter | Type | Required | Description |
|---|---|---|---|
link | String | No | Contains the request URL for the next page or previous page of records. Example: <https://{handle}.myshopline.com/admin/openapi/{version}/products.json?page_info={page_info}&limit={limit}>;rel="next", <https://{handle}.myshopline.com/admin/openapi/{version}/products.json?page_info={page_info}&limit={limit}>;rel="previous"The following parameters are included:
|
page_info mode
The following table describes the query parameters used in page_info mode:
| Parameter | Type | Required | Description |
|---|---|---|---|
page_info | String | No | The encoded cursor string representing the previous or next page of data under the current query criteria. |
limit | Long | No | The number of records to return per page. The default value depends on the API schema. |
The following table describes the link header:
| Parameter | Type | Required | Description |
|---|---|---|---|
link | String | No | Contains the request URL for the next page or the previous page of records. Example: <https://{handle}.myshopline.com/admin/openapi/{version}/products.json?page_info={page_info}&limit={limit}>;rel="next", <https://{handle}.myshopline.com/admin/openapi/{version}/products.json?page_info={page_info}&limit={limit}>; rel="previous"The following parameters are included:
|
Examples of link field parsing
This section provides utility classes and usage examples for parsing the link header across five common backend languages: Java, PHP, Python, Node.js, and Go.
Java
Utility class
package com.shopline.client.api.internal.util;
import com.shopline.client.api.constant.Constants;
import com.shopline.client.api.model.ListOptions;
import com.shopline.client.api.model.Pagination;
import org.apache.commons.lang3.StringUtils;
import java.net.URI;
import java.net.URLDecoder;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Pagination
* Chinese:https://developer.shopline.com/zh-hans-cn/docs/apps/api-instructions-for-use/paging-mechanism/
* English:https://developer.shopline.com/docs/apps/api-instructions-for-use/paging-mechanism/
*/
public abstract class PaginationUtils {
// link pattern
private static final Pattern PATTERN = Pattern.compile("^ *<([^>]+)>; rel=\"(previous|prev|next)\" *$");
/**
* Parse linkHeader
*
* @param linkHeader
* @return Pagination
* @throws Exception
*/
public static Pagination parse(String linkHeader) throws Exception {
if (StringUtils.isBlank(linkHeader)) {
return null;
}
Pagination pagination = new Pagination();
String[] links = linkHeader.split(",");
for (String link : links) {
Matcher matcher = PATTERN.matcher(link);
if (matcher.matches()) {
String url = matcher.group(1);
String nav = matcher.group(2);
ListOptions option = buildListOption(url);
if ("next".equalsIgnoreCase(nav)) {
pagination.setNext(option);
} else {
pagination.setPrevious(option);
}
}
}
return pagination;
}
// url https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9
private static ListOptions buildListOption(String url) throws Exception {
URI uri = new URI(url);
Map<String, String> queryParams = parseQueryParams(uri.getQuery());
String pageInfo = queryParams.get("page_info");
ListOptions option = new ListOptions();
option.setPageInfo(pageInfo);
String limit = queryParams.get("limit");
String sinceId = queryParams.get("since_id");
String fields = queryParams.get("fields");
if (StringUtils.isNotBlank(sinceId)) {
option.setSinceId(Long.parseLong(sinceId));
}
option.setFields(fields);
if (StringUtils.isNotBlank(limit)) {
option.setLimit(Integer.parseInt(limit));
}
return option;
}
private static Map<String, String> parseQueryParams(String query) throws Exception {
if (query == null || query.isEmpty()) {
return Collections.emptyMap();
}
Map<String, String> params = new HashMap<>();
String[] pairs = query.split("&");
for (String pair : pairs) {
String[] keyValue = pair.split("=", 2);
String key = URLDecoder.decode(keyValue[0], Constants.DEFAULT_ENCODING);
String value = keyValue.length > 1 ? URLDecoder.decode(keyValue[1], Constants.DEFAULT_ENCODING) : "";
params.put(key, value);
}
return params;
}
}
Usage examples
// Parse next
String linkHeader = "<https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9>; rel=\"next\"";
Pagination pagination = PaginationUtils.parse(linkHeader);
// Parse next && previous
String linkHeader = "<https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9>; rel=\"next\",<https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9>; rel=\"previous\"";
Pagination pagination = PaginationUtils.parse(linkHeader);
PHP
Utility class
<?php
declare(strict_types=1);
namespace Shopline\Client;
// Pagination of results
// For more details, see:
// https://developer.shopline.com/docs/apps/api-instructions-for-use/paging-mechanism
class PaginationParser
{
// linkRegex parses pagination links from SHOPLINE API responses.
private const link_REGEX = '/ *<([^>]+)>; rel="(previous|prev|next)" */';
/**
* Parse link header from a SHOPLINE API response.
*
* Example linkHeader:
* <https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=xxx>; rel="next"
*
* @param string $linkHeader
* @return Pagination|null
* @throws ResponseDecodingError
*/
public static function parsePagination(string $linkHeader): ?Pagination
{
if ($linkHeader === '') {
return null;
}
$pagination = new Pagination();
foreach (explode(',', $linkHeader) as $link) {
self::parseListOptions($link, $pagination);
}
return $pagination;
}
/**
* @throws ResponseDecodingError
*/
private static function parseListOptions(string $link, Pagination $pagination): void
{
if (!preg_match(self::link_REGEX, $link, $matches)) {
// We expect $matches[1] = URL, $matches[2] = 'previous'|'prev'|'next'
throw new ResponseDecodingError('could not extract pagination link header');
}
$urlStr = $matches[1];
$rel = $matches[2];
$parsed = parse_url($urlStr);
if ($parsed === false || empty($parsed['scheme'])) {
throw new ResponseDecodingError('pagination does not contain a valid URL');
}
parse_str($parsed['query'] ?? '', $params);
if (empty($params['page_info'])) {
throw new ResponseDecodingError('The page_info is missing');
}
$listOptions = new ListOptions();
$listOptions->pageInfo = $params['page_info'];
if (isset($params['limit']) && $params['limit'] !== '') {
$listOptions->limit = (int) $params['limit'];
}
if (!empty($params['fields'])) {
$listOptions->fields = $params['fields'];
}
if ($rel === 'next') {
$pagination->next = $listOptions;
} elseif ($rel === 'previous' || $rel === 'prev') {
$pagination->previous = $listOptions;
} else {
throw new \InvalidArgumentException("Invalid pagination link format, rel: {$rel}");
}
}
}
Usage examples
<?php
declare(strict_types=1);
require_once __DIR__ . '/../src/ResponseDecodingError.php';
require_once __DIR__ . '/../src/ListOptions.php';
require_once __DIR__ . '/../src/Pagination.php';
require_once __DIR__ . '/../src/PaginationParser.php';
use Shopline\Client\PaginationParser;
use Shopline\Client\ResponseDecodingError;
$passed = 0;
$failed = 0;
function ok(bool $cond, string $label): void
{
global $passed, $failed;
if ($cond) {
echo "PASS: {$label}\n";
$passed++;
} else {
echo "FAIL: {$label}\n";
$failed++;
}
}
// Case 1: invalid header string → throws ResponseDecodingError
try {
PaginationParser::parsePagination('linkHeader');
ok(false, 'case1 - invalid header should throw');
} catch (ResponseDecodingError $e) {
ok(true, 'case1 - invalid header throws ResponseDecodingError');
}
// Case 2: empty string → returns null
$result = PaginationParser::parsePagination('');
ok($result === null, 'case2 - empty header returns null');
// Case 3: both next and previous links
$linkHeader =
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' .
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9' .
'>; rel="next",' .
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' .
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9' .
'>; rel="previous"';
$pagination = PaginationParser::parsePagination($linkHeader);
ok($pagination !== null, 'case3 - pagination not null');
ok($pagination->previous !== null, 'case3 - previous not null');
ok(
$pagination->previous->pageInfo === 'eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9',
'case3 - previous pageInfo correct',
);
ok($pagination->previous->limit === 1, 'case3 - previous limit = 1');
ok($pagination->next !== null, 'case3 - next not null');
ok(
$pagination->next->pageInfo === 'eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9',
'case3 - next pageInfo correct',
);
ok($pagination->next->limit === 1, 'case3 - next limit = 1');
// Case 4: only next link
$linkHeader =
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' .
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9' .
'>; rel="next"';
$pagination = PaginationParser::parsePagination($linkHeader);
ok($pagination !== null, 'case4 - pagination not null');
ok($pagination->next !== null, 'case4 - next not null');
ok($pagination->next->pageInfo !== '', 'case4 - next pageInfo not empty');
ok($pagination->next->limit === 1, 'case4 - next limit = 1');
// Case 5: rel="prev" treated as previous
$linkHeader =
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' .
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9' .
'>; rel="next",' .
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' .
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9' .
'>; rel="prev"';
$pagination = PaginationParser::parsePagination($linkHeader);
ok($pagination !== null, 'case5 - pagination not null');
ok($pagination->previous !== null, 'case5 - previous not null (rel=prev)');
ok($pagination->next !== null, 'case5 - next not null');
// Summary
echo "\n{$passed} passed, {$failed} failed\n";
exit($failed > 0 ? 1 : 0);
Python
Utility class
// Code content
import re
from dataclasses import dataclass, field
from typing import Optional
from urllib.parse import urlparse, parse_qs
# Pagination of results
# For more details, see:
# https://developer.shopline.com/docs/apps/api-instructions-for-use/paging-mechanism
class ResponseDecodingError(Exception):
def __init__(self, message: str):
self.message = message
super().__init__(message)
@dataclass
class ListOptions:
# PageInfo is used with new pagination search.
page_info: str = ""
since_id: Optional[int] = None
limit: int = 0
fields: str = ""
@dataclass
class Pagination:
next: Optional[ListOptions] = None
previous: Optional[ListOptions] = None
# linkRegex is used to parse the pagination link from SHOPLINE API search results.
_link_REGEX = re.compile(r'^ *<([^>]+)>; rel="(previous|prev|next)" *$')
def parse_pagination(link_header: str) -> Optional[Pagination]:
"""Parse link header from SHOPLINE API response.
Example link_header:
<https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=xxx>; rel="next"
"""
if not link_header:
return None
pagination = Pagination()
for link in link_header.split(","):
_parse_list_options(link, pagination)
return pagination
def _parse_list_options(link: str, pagination: Pagination) -> None:
match = _link_REGEX.match(link)
# We expect match[1] = URL, match[2] = 'previous'|'prev'|'next'
if not match:
raise ResponseDecodingError("could not extract pagination link header")
url_str = match.group(1)
rel = match.group(2)
try:
parsed_url = urlparse(url_str)
if not parsed_url.scheme:
raise ValueError("no scheme")
except Exception:
raise ResponseDecodingError("pagination does not contain a valid URL")
params = parse_qs(parsed_url.query)
list_options = ListOptions()
page_info_values = params.get("page_info", [])
if not page_info_values:
raise ResponseDecodingError("The page_info is missing")
list_options.page_info = page_info_values[0]
limit_values = params.get("limit", [])
if limit_values:
list_options.limit = int(limit_values[0])
fields_values = params.get("fields", [])
if fields_values:
list_options.fields = fields_values[0]
if rel == "next":
pagination.next = list_options
elif rel in ("previous", "prev"):
pagination.previous = list_options
else:
raise ValueError(f"Invalid pagination link format, rel: {rel}")
Usage examples
// Code content
import sys
import os
import unittest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from client.pagination import parse_pagination, ResponseDecodingError
class TestParsePagination(unittest.TestCase):
def test_next_and_previous(self):
# Case 1: both next and previous links
link_header = (
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json'
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9'
'>; rel="next",'
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json'
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9'
'>; rel="previous"'
)
pagination = parse_pagination(link_header)
self.assertIsNotNone(pagination)
self.assertIsNotNone(pagination.previous)
self.assertEqual(
pagination.previous.page_info,
"eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9",
)
self.assertEqual(pagination.previous.limit, 1)
self.assertIsNotNone(pagination.next)
self.assertEqual(
pagination.next.page_info,
"eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9",
)
self.assertEqual(pagination.next.limit, 1)
def test_next_only(self):
# Case 2: only next link
link_header = (
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json'
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9'
'>; rel="next"'
)
pagination = parse_pagination(link_header)
self.assertIsNotNone(pagination)
self.assertIsNotNone(pagination.next)
self.assertNotEqual(pagination.next.page_info, "")
self.assertEqual(pagination.next.limit, 1)
def test_rel_prev_alias(self):
# Case 3: rel="prev" treated as previous
link_header = (
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json'
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9'
'>; rel="next",'
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json'
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9'
'>; rel="prev"'
)
pagination = parse_pagination(link_header)
self.assertIsNotNone(pagination)
self.assertIsNotNone(pagination.previous)
self.assertIsNotNone(pagination.next)
if __name__ == "__main__":
unittest.main()
Node.js
Utility class
// Code content
// Pagination of results
// For more details, see:
// https://developer.shopline.com/docs/apps/api-instructions-for-use/paging-mechanism
export class ResponseDecodingError extends Error {
constructor(message) {
super(message);
this.name = 'ResponseDecodingError';
}
}
export class ListOptions {
/** @type {string} */
pageInfo = '';
/** @type {number|null} */
sinceId = null;
/** @type {number} */
limit = 0;
/** @type {string} */
fields = '';
}
export class Pagination {
/** @type {ListOptions|null} */
next = null;
/** @type {ListOptions|null} */
previous = null;
}
// linkRegex parses pagination links from SHOPLINE API responses.
const link_REGEX = /^ *<([^>]+)>; rel="(previous|prev|next)" *$/;
/**
* Parse link header from a SHOPLINE API response.
*
* Example linkHeader:
* <https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=xxx>; rel="next"
*
* @param {string} linkHeader
* @returns {Pagination|null}
*/
export function parsePagination(linkHeader) {
if (!linkHeader) {
return null;
}
const pagination = new Pagination();
for (const link of linkHeader.split(',')) {
_parseListOptions(link, pagination);
}
return pagination;
}
/**
* @param {string} link
* @param {Pagination} pagination
*/
function _parseListOptions(link, pagination) {
const match = link_REGEX.exec(link);
// We expect match[1] = URL, match[2] = 'previous'|'prev'|'next'
if (!match) {
throw new ResponseDecodingError('could not extract pagination link header');
}
let parsedURL;
try {
parsedURL = new URL(match[1]);
} catch {
throw new ResponseDecodingError('pagination does not contain a valid URL');
}
const pageInfo = parsedURL.searchParams.get('page_info');
if (!pageInfo) {
throw new ResponseDecodingError('The page_info is missing');
}
const listOptions = new ListOptions();
listOptions.pageInfo = pageInfo;
const limit = parsedURL.searchParams.get('limit');
if (limit !== null) {
listOptions.limit = parseInt(limit, 10);
}
const fields = parsedURL.searchParams.get('fields');
if (fields !== null) {
listOptions.fields = fields;
}
const rel = match[2];
if (rel === 'next') {
pagination.next = listOptions;
} else if (rel === 'previous' || rel === 'prev') {
pagination.previous = listOptions;
} else {
throw new Error(`Invalid pagination link format, rel: ${rel}`);
}
}
Usage examples
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { parsePagination, ResponseDecodingError } from '../client/pagination.js';
// Case 1: both next and previous links
test('parsePagination - next and previous', () => {
const linkHeader =
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' +
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9' +
'>; rel="next",' +
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' +
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9' +
'>; rel="previous"';
const pagination = parsePagination(linkHeader);
assert.ok(pagination);
assert.ok(pagination.previous);
assert.equal(
pagination.previous.pageInfo,
'eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9',
);
assert.equal(pagination.previous.limit, 1);
assert.ok(pagination.next);
assert.equal(
pagination.next.pageInfo,
'eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9',
);
assert.equal(pagination.next.limit, 1);
});
// Case 2: only next link
test('parsePagination - next only', () => {
const linkHeader =
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' +
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9' +
'>; rel="next"';
const pagination = parsePagination(linkHeader);
assert.ok(pagination);
assert.ok(pagination.next);
assert.ok(pagination.next.pageInfo);
assert.equal(pagination.next.limit, 1);
});
// Case 3: rel="prev" treated as previous
test('parsePagination - rel=prev alias', () => {
const linkHeader =
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' +
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9' +
'>; rel="next",' +
'<https://example.myshopline.com/admin/openapi/v20270301/products/products.json' +
'?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9' +
'>; rel="prev"';
const pagination = parsePagination(linkHeader);
assert.ok(pagination);
assert.ok(pagination.previous);
assert.ok(pagination.next);
});
Go
Utility class
For the source code of the parsing utility, refer to shopline-sdk-go/client/pagination.go.
package client
import (
"errors"
"fmt"
"net/url"
"regexp"
"strconv"
"strings"
)
// Pagination of results
// For more details, see:
// Chinese: https://developer.shopline.com/zh-hans-cn/docs/apps/api-instructions-for-use/paging-mechanism
// English: https://developer.shopline.com/docs/apps/api-instructions-for-use/paging-mechanism
type Pagination struct {
Next *ListOptions
Previous *ListOptions
}
// ListOptions
// General list options that can be used for most collections of entities.
type ListOptions struct {
// PageInfo is used with new pagination search.
PageInfo string `url:"page_info,omitempty"`
SinceId *uint64 `url:"since_id,omitempty"`
Limit int `url:"limit,omitempty"`
Fields string `url:"fields,omitempty"`
}
// linkRegex is used to parse the pagination link from SHOPLINE API search results.
var linkRegex = regexp.MustCompile(`^ *<([^>]+)>; rel="(previous|prev|next)" *$`)
// ParsePagination
// linkHeader eg: <https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9>; rel="next"
// Chinese: https://developer.shopline.com/zh-hans-cn/docs/apps/api-instructions-for-use/paging-mechanism
// English: https://developer.shopline.com/docs/apps/api-instructions-for-use/paging-mechanism
func ParsePagination(linkHeader string) (*Pagination, error) {
if linkHeader == "" {
return nil, nil
}
pagination := new(Pagination)
for _, link := range strings.Split(linkHeader, ",") {
err := parseListOptions(link, pagination)
if err != nil {
return nil, err
}
}
return pagination, nil
}
// link <https://{handle}.myshopline.com/admin/openapi/{version}/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9>; rel="next"
func parseListOptions(link string, pagination *Pagination) error {
match := linkRegex.FindStringSubmatch(link)
// Make sure the link is not empty or invalid
if len(match) != 3 {
// We expect 3 values:
// match[0] = full match
// match[1] is the URL and match[2] is either 'previous' or 'next'
err := ResponseDecodingError{
Message: "could not extract pagination link header",
}
return err
}
queryURL, err := url.Parse(match[1])
if err != nil {
err = ResponseDecodingError{
Message: "pagination does not contain a valid URL",
}
return err
}
params, err := url.ParseQuery(queryURL.RawQuery)
if err != nil {
return err
}
paginationListOptions := ListOptions{}
paginationListOptions.PageInfo = params.Get("page_info")
if paginationListOptions.PageInfo == "" {
err = ResponseDecodingError{
Message: "The page_info is missing",
}
return err
}
limit := params.Get("limit")
if limit != "" {
paginationListOptions.Limit, err = strconv.Atoi(params.Get("limit"))
if err != nil {
return err
}
}
fields := params.Get("fields")
if fields != "" {
paginationListOptions.Fields = params.Get("fields")
}
// 'rel' is either next or previous
if match[2] == "next" {
pagination.Next = &paginationListOptions
} else if match[2] == "previous" || match[2] == "prev" {
pagination.Previous = &paginationListOptions
} else {
return errors.New(fmt.Sprintf("Invalid pagination link format, rel: %s", match[2]))
}
return nil
}
Usage examples
package client
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestParsePagination(t *testing.T) {
// Case 1
linkHeader := "linkHeader"
_, err := ParsePagination(linkHeader)
a := assert.New(t)
a.NotNil(err)
// Case 2
linkHeader = ""
_, err = ParsePagination(linkHeader)
a.Nil(err)
// Case 3
linkHeader = "<https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9>; rel=\"next\",<https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9>; rel=\"previous\""
pagination, err := ParsePagination(linkHeader)
a.Nil(err)
a.NotNil(pagination)
a.NotNil(pagination.Previous)
a.Equal(pagination.Previous.PageInfo, "eyJzaW5jZUlkIjoiMTYwNTc2NjAxNzI1MjczOTI4MDEwOTI3ODgiLCJkaXJlY3Rpb24iOiJwcmV2IiwibGltaXQiOjF9")
a.Equal(pagination.Previous.Limit, 1)
a.NotNil(pagination.Next)
a.NotEmpty(pagination.Next.PageInfo)
a.Equal(pagination.Next.PageInfo, "eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9")
a.Equal(pagination.Next.Limit, 1)
// Case 4
linkHeader = "<https://example.myshopline.com/admin/openapi/v20270301/products/products.json?limit=1&page_info=eyJzaW5jZUlkIjoiMTYwNTc1OTAxNTM4OTA4Mjk1MjExMTI3ODgiLCJkaXJlY3Rpb24iOiJuZXh0IiwibGltaXQiOjF9>; rel=\"next\""
pagination, err = ParsePagination(linkHeader)
a.Nil(err)
a.NotNil(pagination)
a.NotNil(pagination.Next)
a.NotEmpty(pagination.Next.PageInfo)
a.Equal(pagination.Next.Limit, 1)
}