A boundary that keeps cursor channels apart
Within the documented v1 surface, list pagination and search pagination use separate continuation mechanisms. List cursors are object identifiers supplied through one of two mutually exclusive fields; search continuation instead uses a token returned in a search response.
Search begins with no page field, then a later request supplies the earlier response token in page. For both endpoint families, treat has_more as the documented availability signal for another result page, rather than as an application promise about a stable dataset or recoverable stored state.
Use an application-owned tagged record that keeps endpoint family, resource or query identity, direction or phase, and cursor origin together. This is a proposed boundary design, not a provider-prescribed implementation. Its purpose is to make an invalid cursor route unrepresentable, or at least rejectable, before an HTTP request is assembled.
Four request decisions
| Case | Endpoint family | Traversal direction or phase | Cursor provenance | Permitted request parameter | Documented ordering or stopping evidence | Invalid combination to reject | Bounded application-owned action |
|---|---|---|---|---|---|---|---|
| Advance a list from its final returned item | List | Forward | An object ID taken from the final item in the received list page | starting_after | Newest-created records come first; when has_more becomes false, end traversal. | Adding ending_before to the same request | Build a forward-list request only after the record is tagged as a list object-ID cursor. |
| Move a list toward the prior page | List | Backward | An object ID taken from the first item in the received list page | ending_before | The request selects records before the supplied object; list ordering is reverse by creation time. | Adding starting_after to the same request | Require an explicit backward-list action and retain its direction in state. |
| Start a search | Search | Initial phase | No cursor exists yet | Omit page | No page parameter belongs on the initial search; stop when has_more is false. | Supplying any saved continuation token as page on the initial phase | Create an initial-search state without a cursor and wait for the response. |
| Continue a search after a result page | Search | Continuation phase | The next_page value returned by the preceding search response | page | Use the returned token as page when has_more indicates another page. | Using a list object ID in page, or routing a search token into a list field | Accept only a search-tagged continuation record that matches the active search operation. |
The table deliberately separates the source-defined request shapes from local policy. In particular, preserving direction and checking resource or query identity are application safeguards; they are not claims that a cursor provides durable checkpoint or consistency semantics.
Hypothetical mismatch walkthrough
Hypothetical example: a client has retained a continuation token from a search response. A caller accidentally sends that record to the list-navigation route. The proposed boundary sees the search tag where a list object-ID cursor is required, rejects the mismatch before constructing a request, leaves the caller's existing traversal state unchanged, and asks the caller to choose the corresponding search-continuation operation.
This is a local failure-handling rule, not a reported execution or a recovery guarantee. It avoids converting a search token into starting_after or ending_before; the caller can instead resume only through the matching search path after applying whatever application validation policy is appropriate.
Alternatives and scope limits
A single untyped cursor string is possible, but it shifts family, phase, and provenance checks to every request-construction site. A tagged boundary centralizes those checks and makes the supported request field a consequence of the record type. If a simpler design is necessary, retain an explicit endpoint-family discriminator alongside the string and validate it before selecting a parameter.
This article is limited to the documented Stripe v1 list and search behavior. The API reference identifies pagination under /v2 as a different interface, so it needs a separate boundary review. SDK auto-pagination, performance tuning, durable import checkpoints, and guarantees about concurrent dataset changes are intentionally outside this design.
The rejection boundary needs an explicit recovery invariant, not only a type distinction. Consider a caller that persists its traversal record, submits a search token to a list action, receives a local rejection, and retries the intended search action. How does the boundary prove that the rejected attempt did not mutate the stored cursor, traversal direction, query identity, or outbound-request queue?
This matters because the documented shapes are incompatible: list pagination accepts object IDs in mutually exclusive
starting_afterorending_before, while search continuation uses the prior search response’snext_pageinpage. Stripe’s pagination reference documents those separate fields, but it does not establish the application’s rollback behavior after a routing error.A useful acceptance test would assert all three outcomes for the mismatch:
Without that last check, a correct preflight rejection can still strand the caller if an attempted list transition consumes or overwrites the only retained search continuation.