---
title: "REST Admin API pagination"

doc_type: static
doc_category: static
doc_version: v20270301
source_url:
  html: 'https://developer.shopline.com/docs/apps/api-instructions-for-use/paging-mechanism'
  md: 'https://developer.shopline.com/docs/apps/api-instructions-for-use/paging-mechanism.md'
---
# 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](https://developer.shopline.com/docs/admin-rest-api/product/product/get-products.md) API with `limit=10` in the query parameters:

```latex
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:

```latex
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:

```latex
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:

```latex
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"`:

```latex
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:

| **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.<br />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.<br />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"`<br />The following parameters are included:<ul><li><code>handle</code>: The unique identifier for the store.</li><li><code>version</code>: The API version.</li><li><code>page_info</code>: The encoded cursor string representing the previous or next page of data under the current query criteria.</li><li><code>limit</code>: The maximum number of records returned per page.</li></ul> |

### `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.<br />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"`<br />The following parameters are included:<ul><li><code>handle</code>: The unique identifier for the store.</li><li><code>version</code>: The API version.</li><li><code>page_info</code>: The encoded cursor string representing the previous or next page under the current query criteria.</li><li><code>limit</code>: The maximum number of records returned per page.</li></ul> |

## 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

```java
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

```java
// 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
<?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
<?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

```python
// 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

```python
// 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

```javascript
// 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

```javascript
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](https://github.com/shoplineos/shopline-sdk-go/blob/main/client/pagination.go).

```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

```go
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)

}
```