Every list endpoint in AppEngine uses the same pagination and sort conventions. Learn them once.
Query-string pagination (p, ps)
GET endpoints — find-page-data, find-by-attribute, search, marketing/campaigns, client-data/orders, etc. — accept the same four params:
| Field | Type | Description |
|---|---|---|
| p | number | Page number. 1-based. Default |
| ps | number | Page size. Default |
| s | string | Sort field. Default |
| st | 'asc' | 'desc' | Sort direction. Default |
curl 'https://appengine.appmint.io/repository/find-page-data/product?p=2&ps=20&s=data.price&st=asc' \
-H "orgid: my-org"
Body pagination (options)
POST endpoints — find, search, aggregate, dynamic-query/find — take the same fields inside an options object:
{
"query": { "data.status": "active" },
"options": {
"page": 2,
"pageSize": 20,
"sort": "data.createdAt",
"sortType": "desc"
}
}
Some endpoints also accept the MongoDB-driver shape { limit, skip, sort: { 'data.x': -1 } }. The dynamic-query controller passes these straight through:
{
"collection": "lead",
"filter": { "data.score": { "$gte": 70 } },
"options": { "limit": 50, "skip": 100, "sort": { "data.score": -1 } }
}
Response envelope
Paged endpoints wrap the results in:
{
data: BaseModel\<T\>[], // or an array of payloads, depending on endpoint
total: number, // total count matching the query (across all pages)
hasNext: boolean, // convenience; true when (p * ps) < total
page?: number,
pageSize?: number,
totalPages?: number,
}
total is exact, not estimated. It's computed from a count against the same filter. For very large collections, this adds a small overhead — pass ?count=false (where supported) to skip it.
Total count behavior
The platform returns exact totals because most UIs (admin tables, customer order history, lead lists) expect them. There is no "fast count" mode that returns an estimate.
If you only need the next page and don't care about totals, use the hasNext flag in the response and ignore total — the count step still runs but you save the round-trip you'd otherwise spend on count separately.
Cursor vs offset
AppEngine list endpoints are offset-paginated. There is no opaque cursor token. This is fine for typical UI tables (sorted by modifydate desc, page sizes <= 100). Two consequences:
- Deep pages are slower than shallow ones —
?p=500reads 500 pages of records to skip them. Avoid it. - Concurrent inserts can cause skips/dupes between pages. If you need consistency across a long iteration, snapshot a
modifydateupper bound at the start and add it to your filter.
The find-page-data endpoint also supports a lastItem (?l=...) parameter for keyset-style pagination on indexed fields. Pass the sk of the last record on the previous page; the server returns records strictly after it, in the current sort order. This is faster than offset for streaming through big collections.
# first page
curl 'https://appengine.appmint.io/repository/find-page-data/contact?ps=100&s=createdate&st=asc'
# next page using last sk
curl 'https://appengine.appmint.io/repository/find-page-data/contact?ps=100&s=createdate&st=asc&l=contact-abc-123'
Sort fields
Common sort fields are indexed:
modifydate,createdate— every collectionversion— every collectionstate— every collectiondata.name,data.title— most content collectionsdata.price,data.sku— productsdata.score— leads, opportunities
Sorting on an unindexed data.* field works but scans the collection. For large collections, talk to platform support about adding an index, or pre-compute a sort key into data.
Multi-field sort
The query-string convention only supports one field. For multi-field sorts, use the body shape (options.sort) with a Mongo-style document:
{
"options": {
"sort": { "data.priority": -1, "createdate": -1 }
}
}
-1 for descending, 1 for ascending — matches the MongoDB driver.
Examples by domain
# storefront product list, cheapest first
curl 'https://appengine.appmint.io/storefront/products?p=1&ps=24&s=data.price&st=asc' \
-H "orgid: my-org"
# customer order history, newest first
curl 'https://appengine.appmint.io/client-data/orders?p=1&ps=10' \
-H "orgid: my-org" -H "Authorization: Bearer $JWT"
# CRM leads, highest score first
curl 'https://appengine.appmint.io/crm/leads?p=1&ps=20&s=data.score&st=desc' \
-H "orgid: my-org" -H "Authorization: Bearer $JWT"
The convention is the same across /repository/*, /storefront/*, /crm/*, /client-data/*, /marketing/*, and /finance/*. Once you've paginated one, you've paginated them all.