# Pagination > Cursor pagination for list endpoints — limit, starting_after, ending_before and has_more. Source: https://swaglists.com/docs/pagination List endpoints (`GET /orders`, `GET /products`, `GET /catalog/products`, `GET /events`, …) return a **list object** and use cursor pagination: ```json { "object": "list", "data": [ { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa" } ], "has_more": true, "url": "/api/v1/orders" } ``` | Parameter | Meaning | | --- | --- | | `limit` | Objects per page, 1–100 (default 20). | | `starting_after` | An object id: return the page **after** it (the next page). Use the `id` of the last object you received. | | `ending_before` | An object id: return the page **before** it (the previous page). Use the `id` of the first object of the current page. | Use one cursor at a time (`400 parameter_invalid` if you send both). `has_more` tells you whether another page exists in the direction you are paging. Lists are newest first (except order events, which are oldest first). Filters (for example `status` on orders) combine with cursors — keep the same filters while paging. Query parameters are **strict**: a parameter an endpoint does not document is rejected with `400 parameter_invalid` naming it (`Received unknown parameter: updated_since (query parameter).`), so a misspelled filter never silently returns everything. The one exception is `_`, which every endpoint accepts and ignores (for cache-busters added by HTTP tools). ## Cursors A cursor is the `id` of an object of the same list, so it carries the list's id prefix: | List | Cursor prefix | | --- | --- | | `GET /orders` | `ord_` | | `GET /products` | `prod_` | | `GET /catalog/products` | `cp_` | | `GET /files` | `file_` | | `GET /events` | `evt_` | | `GET /wallet/transactions` | `txn_` | | `GET /topups` | `top_` | | `GET /webhook_endpoints` | `we_` | A cursor with another prefix (for example a product id on `/orders`) is a `400 parameter_invalid` on `starting_after` / `ending_before`. A well-formed id that does not exist (an object deleted since) is accepted: the page simply starts after (or ends before) where that id would sort. The catalog is the exception — its order is curated (featured first), so `/catalog/products` cursors must be ids of products in the catalog. ## Iterate over everything ```js title="Node.js" async function* listAll(path, params = {}) { let startingAfter; while (true) { const query = new URLSearchParams({ ...params, limit: "100", ...(startingAfter ? { starting_after: startingAfter } : {}) }); const res = await fetch(`https://swaglists.com/api/v1${path}?${query}`, { headers: { Authorization: `Bearer ${process.env.API_KEY}` } }); const page = await res.json(); if (!res.ok) throw new Error(`${page.error.code}: ${page.error.message}`); yield* page.data; if (!page.has_more || page.data.length === 0) return; startingAfter = page.data.at(-1).id; } } for await (const order of listAll("/orders", { status: "on_hold" })) { console.log(order.id, order.hold_reason); } ``` ```python title="Python" def list_all(path, **params): starting_after = None while True: query = {**params, "limit": 100} if starting_after: query["starting_after"] = starting_after res = requests.get("https://swaglists.com/api/v1" + path, params=query, headers={"Authorization": f"Bearer {os.environ['API_KEY']}"}) res.raise_for_status() page = res.json() yield from page["data"] if not page["has_more"] or not page["data"]: return starting_after = page["data"][-1]["id"] for order in list_all("/orders", status="on_hold"): print(order["id"], order["hold_reason"]) ``` ```bash title="cURL" curl "https://swaglists.com/api/v1/orders?limit=100" -H "Authorization: Bearer $API_KEY" # next page: curl "https://swaglists.com/api/v1/orders?limit=100&starting_after=ord_01k8x4b2q9w7e3r5t1y8u6i0pa" -H "Authorization: Bearer $API_KEY" ``` ## Polling for new objects To fetch only what is new since your last sync, page **backwards** from the newest object you already have: `GET /events?ending_before=` returns newer events. Or filter by time: `GET /orders?created[gte]=2026-10-20T00:00:00Z` (see [time filters](#time-filters)). For near-real-time updates, prefer [webhooks](https://swaglists.com/docs/webhooks.md). ## Time filters `created[gte]` (at or after) and `created[lte]` (at or before) on `GET /orders` and `GET /events` accept: | Format | Example | Meaning | | --- | --- | --- | | Unix seconds | `1761955200` | Seconds since 1970-01-01 UTC (fractions allowed). | | Unix milliseconds | `1761955200000` | Any number above 100000000000 is read as milliseconds. | | ISO-8601 date | `2026-10-01` | Midnight **UTC** of that day. | | ISO-8601 date-time with an offset | `2026-10-01T09:30:00Z`, `2026-10-01T11:30:00+02:00` | That instant. | | ISO-8601 date-time without an offset | `2026-10-01T09:30:00` | Read as **UTC** — never as the server's or your local time. | URL-encode `+` offsets (`%2B02:00`); an unencoded `+` (which query strings turn into a space) is understood as well. Anything else — other formats, impossible dates such as `2026-02-30`, years outside 2000–2100 — is a `400 parameter_invalid` on the parameter. ## Full collections A few endpoints return a complete collection in the same list shape with `has_more: false` and no paging: `GET /catalog/categories`, `GET /products/{id}/variants`, `GET /orders/{id}/events` and `POST /shipping/rates`.