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_id mode: Used for initial requests (the first page), where since_id specifies the starting point for data retrieval.
  • page_info mode: Used for subsequent requests (non-first pages), where page_info is 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 the link HTTP 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.
CAUTION

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

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"
TIP

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 page
  • rel="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.

TIP

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:

ParameterTypeRequiredDescription
since_idStringNoThe starting resource ID, such as a product ID or order ID. Records with IDs greater than this value will be returned.
Default value: 0
limitLongNoThe number of records to return per page. The default value depends on the API schema.

The following table describes the link header:

ParameterTypeRequiredDescription
linkStringNoContains 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:
  • handle: The unique identifier for the store.
  • version: The API version.
  • page_info: The encoded cursor string representing the previous or next page of data under the current query criteria.
  • limit: The maximum number of records returned per page.

page_info mode​

The following table describes the query parameters used in page_info mode:

ParameterTypeRequiredDescription
page_infoStringNoThe encoded cursor string representing the previous or next page of data under the current query criteria.
limitLongNoThe number of records to return per page. The default value depends on the API schema.

The following table describes the link header:

ParameterTypeRequiredDescription
linkStringNoContains 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:
  • handle: The unique identifier for the store.
  • version: The API version.
  • page_info: The encoded cursor string representing the previous or next page under the current query criteria.
  • limit: The maximum number of records returned per page.

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)
}
Was this article helpful to you?