diff --git a/mypy.ini b/mypy.ini
--- a/mypy.ini
+++ b/mypy.ini
@@ -43,3 +43,6 @@
 
 [mypy-systemd.*]
 ignore_missing_imports = True
+
+[mypy-attrs_strict.*]
+ignore_missing_imports = True
diff --git a/requirements.txt b/requirements.txt
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,3 +2,4 @@
 Deprecated
 PyYAML
 sentry-sdk
+attrs_strict >= 0.0.7
diff --git a/swh/core/api/classes.py b/swh/core/api/classes.py
new file mode 100644
--- /dev/null
+++ b/swh/core/api/classes.py
@@ -0,0 +1,25 @@
+# Copyright (C) 2020  The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+import attr
+
+from typing import (
+    Generic,
+    List,
+    Optional,
+    TypeVar,
+)
+
+
+ResultType = TypeVar("ResultType")
+TokenType = TypeVar("TokenType")
+
+
+@attr.s
+class PagedResult(Generic[ResultType, TokenType]):
+    """Represents a page of results; with a token to get the next page"""
+
+    results = attr.ib(type=List[ResultType], default=[])
+    next_page_token = attr.ib(type=Optional[TokenType], default=None)  # type: ignore
diff --git a/swh/core/api/serializers.py b/swh/core/api/serializers.py
--- a/swh/core/api/serializers.py
+++ b/swh/core/api/serializers.py
@@ -18,6 +18,8 @@
 from typing import Any, Dict, Union, Tuple
 from requests import Response
 
+from swh.core.api.classes import PagedResult
+
 
 def encode_datetime(dt: datetime.datetime) -> str:
     """Wrapper of datetime.datetime.isoformat() that forbids naive datetimes."""
@@ -26,6 +28,19 @@
     return dt.isoformat()
 
 
+def _encode_paged_result(obj: PagedResult) -> Dict[str, Any]:
+    """Serialize PagedResult to a Dict."""
+    return {
+        "results": obj.results,
+        "next_page_token": obj.next_page_token,
+    }
+
+
+def _decode_paged_result(obj: Dict[str, Any]) -> PagedResult:
+    """Deserialize Dict into PagedResult"""
+    return PagedResult(results=obj["results"], next_page_token=obj["next_page_token"],)
+
+
 ENCODERS = [
     (arrow.Arrow, "arrow", arrow.Arrow.isoformat),
     (datetime.datetime, "datetime", encode_datetime),
@@ -39,6 +54,7 @@
         },
     ),
     (UUID, "uuid", str),
+    (PagedResult, "paged_result", _encode_paged_result),
     # Only for JSON:
     (bytes, "bytes", lambda o: base64.b85encode(o).decode("ascii")),
 ]
@@ -48,6 +64,7 @@
     "datetime": lambda d: iso8601.parse_date(d, default_timezone=None),
     "timedelta": lambda d: datetime.timedelta(**d),
     "uuid": UUID,
+    "paged_result": _decode_paged_result,
     # Only for JSON:
     "bytes": base64.b85decode,
 }
diff --git a/swh/core/api/tests/test_serializers.py b/swh/core/api/tests/test_serializers.py
--- a/swh/core/api/tests/test_serializers.py
+++ b/swh/core/api/tests/test_serializers.py
@@ -6,13 +6,16 @@
 import datetime
 import json
 
-from typing import Any, Callable, List, Tuple
+from typing import Any, Callable, List, Tuple, Union
+
+from arrow import Arrow
 from uuid import UUID
 
 import pytest
 import arrow
 import requests
 
+from swh.core.api.classes import PagedResult
 from swh.core.api.serializers import (
     SWHJSONDecoder,
     SWHJSONEncoder,
@@ -48,33 +51,87 @@
 
 TZ = datetime.timezone(datetime.timedelta(minutes=118))
 
+DATA_BYTES = b"123456789\x99\xaf\xff\x00\x12"
+ENCODED_DATA_BYTES = {"swhtype": "bytes", "d": "F)}kWH8wXmIhn8j01^"}
+
+DATA_DATETIME = datetime.datetime(2015, 3, 4, 18, 25, 13, 1234, tzinfo=TZ,)
+ENCODED_DATA_DATETIME = {
+    "swhtype": "datetime",
+    "d": "2015-03-04T18:25:13.001234+01:58",
+}
+
+DATA_TIMEDELTA = datetime.timedelta(64)
+ENCODED_DATA_TIMEDELTA = {
+    "swhtype": "timedelta",
+    "d": {"days": 64, "seconds": 0, "microseconds": 0},
+}
+
+DATA_ARROW = arrow.get("2018-04-25T16:17:53.533672+00:00")
+ENCODED_DATA_ARROW = {"swhtype": "arrow", "d": "2018-04-25T16:17:53.533672+00:00"}
+
+DATA_UUID = UUID("cdd8f804-9db6-40c3-93ab-5955d3836234")
+ENCODED_DATA_UUID = {"swhtype": "uuid", "d": "cdd8f804-9db6-40c3-93ab-5955d3836234"}
+
+# For test demonstration purposes
+TestPagedResultStr = PagedResult[
+    Union[UUID, datetime.datetime, datetime.timedelta], str
+]
+
+DATA_PAGED_RESULT = TestPagedResultStr(
+    results=[DATA_UUID, DATA_DATETIME, DATA_TIMEDELTA], next_page_token="10",
+)
+
+ENCODED_DATA_PAGED_RESULT = {
+    "d": {
+        "results": [ENCODED_DATA_UUID, ENCODED_DATA_DATETIME, ENCODED_DATA_TIMEDELTA,],
+        "next_page_token": "10",
+    },
+    "swhtype": "paged_result",
+}
+
+TestPagedResultTuple = PagedResult[Union[str, bytes, Arrow], List[Union[str, UUID]]]
+
+
+DATA_PAGED_RESULT2 = TestPagedResultTuple(
+    results=["data0", DATA_BYTES, DATA_ARROW], next_page_token=["10", DATA_UUID],
+)
+
+ENCODED_DATA_PAGED_RESULT2 = {
+    "d": {
+        "results": ["data0", ENCODED_DATA_BYTES, ENCODED_DATA_ARROW,],
+        "next_page_token": ["10", ENCODED_DATA_UUID],
+    },
+    "swhtype": "paged_result",
+}
+
 DATA = {
-    "bytes": b"123456789\x99\xaf\xff\x00\x12",
-    "datetime_tz": datetime.datetime(2015, 3, 4, 18, 25, 13, 1234, tzinfo=TZ,),
+    "bytes": DATA_BYTES,
+    "datetime_tz": DATA_DATETIME,
     "datetime_utc": datetime.datetime(
         2015, 3, 4, 18, 25, 13, 1234, tzinfo=datetime.timezone.utc
     ),
-    "datetime_delta": datetime.timedelta(64),
-    "arrow_date": arrow.get("2018-04-25T16:17:53.533672+00:00"),
+    "datetime_delta": DATA_TIMEDELTA,
+    "arrow_date": DATA_ARROW,
     "swhtype": "fake",
     "swh_dict": {"swhtype": 42, "d": "test"},
     "random_dict": {"swhtype": 43},
-    "uuid": UUID("cdd8f804-9db6-40c3-93ab-5955d3836234"),
+    "uuid": DATA_UUID,
+    "paged-result": DATA_PAGED_RESULT,
+    "paged-result2": DATA_PAGED_RESULT2,
 }
 
 ENCODED_DATA = {
-    "bytes": {"swhtype": "bytes", "d": "F)}kWH8wXmIhn8j01^"},
-    "datetime_tz": {"swhtype": "datetime", "d": "2015-03-04T18:25:13.001234+01:58",},
+    "bytes": ENCODED_DATA_BYTES,
+    "datetime_tz": ENCODED_DATA_DATETIME,
     "datetime_utc": {"swhtype": "datetime", "d": "2015-03-04T18:25:13.001234+00:00",},
-    "datetime_delta": {
-        "swhtype": "timedelta",
-        "d": {"days": 64, "seconds": 0, "microseconds": 0},
-    },
-    "arrow_date": {"swhtype": "arrow", "d": "2018-04-25T16:17:53.533672+00:00"},
+    "datetime_delta": ENCODED_DATA_TIMEDELTA,
+    "arrow_date": ENCODED_DATA_ARROW,
     "swhtype": "fake",
     "swh_dict": {"swhtype": 42, "d": "test"},
     "random_dict": {"swhtype": 43},
-    "uuid": {"swhtype": "uuid", "d": "cdd8f804-9db6-40c3-93ab-5955d3836234"},
+    "uuid": ENCODED_DATA_UUID,
+    "paged-result": ENCODED_DATA_PAGED_RESULT,
+    "paged-result2": ENCODED_DATA_PAGED_RESULT2,
 }