Skip to content

Page Token Extension

Capability URI: https://specs.serverlessinbox.com/page-token

Status: Draft


Standard JMAP query pagination (RFC 8620 §5.5) relies on position (a zero-based integer offset) and anchor (an object id to start from). Both require the server to locate an item within an ordered result set by its absolute or relative index.

This is not possible on backends such as DynamoDB that use opaque cursor tokens for pagination and do not expose index positions. This extension adds a pageToken argument that replaces position and anchor for those backends.

When this proposal is mature enough it will be submitted to the JMAP working group, since the problem applies to any JMAP implementation over a NoSQL key-value store.


A server that supports this extension includes the capability URI in its session capabilities object. No additional capability properties are defined at this time.

{
"capabilities": {
"https://specs.serverlessinbox.com/page-token": {}
}
}

This extension applies to all JMAP */query methods (e.g. Email/query, Mailbox/query, Thread/query).

PropertyTypeDescription
pageTokenstring | nullOpaque cursor returned by a previous query response. Pass null or omit to start from the beginning of the result set.

pageToken is mutually exclusive with position and anchor. If pageToken is provided alongside either of those arguments the server MUST return an invalidArguments error.

PropertyTypeDescription
pageTokenstring | nullOpaque cursor for the next page. null means this is the last page.

The token is opaque to clients. Its format and lifetime are server-defined. Clients MUST NOT construct, modify, or cache tokens beyond a single pagination sequence.


  • The server applies filter and sort exactly as it would without this extension.
  • limit continues to control the maximum number of ids returned per page.
  • calculateTotal MAY be supported; servers that cannot calculate a total without a full scan SHOULD omit total from the response even when calculateTotal is true, and MUST NOT return an error in that case.
  • collapseThreads (on Email/query) works as normal.
  • position in the response SHOULD be 0 when the backend cannot determine the absolute offset of the first returned item.

First page request:

["Email/query", {
"accountId": "u1",
"filter": { "inMailbox": "inbox" },
"sort": [{ "property": "receivedAt", "isAscending": false }],
"limit": 50
}, "r1"]

First page response:

["Email/query/reply", {
"accountId": "u1",
"queryState": "...",
"canCalculateChanges": false,
"position": 0,
"ids": ["e1", "e2", "..."],
"pageToken": "eyJsYXN0S2V5IjoiZTUwIn0"
}, "r1"]

Next page request — pass the pageToken from the response as pageToken in the next request:

["Email/query", {
"accountId": "u1",
"filter": { "inMailbox": "inbox" },
"sort": [{ "property": "receivedAt", "isAscending": false }],
"limit": 50,
"pageToken": "eyJsYXN0S2V5IjoiZTUwIn0"
}, "r2"]

Error typeCondition
invalidArgumentspageToken is provided together with position or anchor.
invalidArgumentspageToken is not a string or is structurally invalid.
serverFailThe token has expired or no longer points to a valid cursor position.

pageToken cursors are not intended for use with */queryChanges. Clients that need to sync incremental changes should use the standard sinceQueryState mechanism. A token-paginated query where canCalculateChanges is false does not support */queryChanges.