API Pagination Standard¶
Version: 1.0 Status: Draft Last Updated: 2026-04-03
Purpose¶
Standardise pagination across all Simpaisa /v3/ APIs. Cursor-based pagination replaces offset-based pagination for better performance on large, frequently changing datasets (270M+ transactions and growing).
Scope¶
Applies to all /v3/ API endpoints returning collections: transaction lists, bank lists, merchant lists, audit logs, and any future list endpoints across Pay-Ins, Pay-Outs, Remittances, and Cards.
Legacy endpoints (pre-v3) retain offset-based pagination for backward compatibility.
Query Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | 20 | Items per page. Min 1, max 100. |
cursor |
string | — | Opaque cursor from previous response. Omit for first page. |
- First page: omit
cursorentirely. - Next page: pass the
cursorvalue from the previous response'spaginationobject. - Clients MUST treat cursor values as opaque strings. Internal encoding may change without notice.
Response Envelope¶
All collection endpoints return this structure:
{
"data": [ ... ],
"pagination": {
"cursor": "eyJjIjoiMjAyNi0wMy0xNVQxMDozMDowMFoiLCJpIjoiYWJjMTIzIn0=",
"hasMore": true,
"total": 1234
}
}
Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
data |
array | Yes | Array of resource objects. |
pagination.cursor |
string | Yes | Cursor for the next page. null when no more results. |
pagination.hasMore |
boolean | Yes | true if more results exist beyond this page. |
pagination.total |
integer | No | Total matching count. Omit if result set exceeds 10,000 — the query cost is not justified. |
Empty Results¶
When no results match the query:
{
"data": [],
"pagination": {
"cursor": null,
"hasMore": false
}
}
Ordering¶
All paginated endpoints use a stable, deterministic sort order:
- Primary:
created_at DESC(newest first). - Tiebreaker:
id ASC— guarantees deterministic ordering when timestamps collide.
This combination ensures no items are skipped or duplicated across pages, even during concurrent writes.
Link Headers¶
Optional. Services SHOULD include a Link header with rel="next" for discoverability:
Link: </v3/transactions?cursor=eyJ...&limit=20>; rel="next"
Omit when hasMore is false.
Implementation Notes¶
- Cursor encoding: base64-encode a JSON object containing the last record's
created_atandid. This keeps cursors opaque to clients while remaining debuggable internally. - Expired cursors: if a cursor references a deleted or archived record, return
400 Bad Requestwith error codeINVALID_CURSOR. - Limit validation: values outside 1–100 return
400 Bad Request. Do not silently clamp. - Filtering: cursors are scoped to the original query filters. Changing filters with an existing cursor returns
400 Bad Request.
Legacy Endpoint Compatibility¶
Pre-v3 endpoints continue to use offset-based pagination (page, per_page) unchanged. No migration is required for existing integrations. New endpoints MUST use cursor-based pagination exclusively.
KrakenD Gateway Configuration¶
KrakenD proxy endpoints should pass cursor and limit query parameters through to backend services without modification. Do not aggregate or manipulate pagination at the gateway layer.
References¶
- Stripe Pagination — industry reference for cursor-based pagination
- Slack API Pagination — cursor pattern example