Pagination
Preview Every list endpoint paginates the same
way: cursor-based, never offset-based. You ask for a page, get an opaque
next_cursor, and pass it back to fetch the next page. There are no page numbers and
no offset — which is what keeps lists stable and fast even over very large,
append-heavy datasets.
Why cursors, not offsets
Section titled “Why cursors, not offsets”Much of what you list — logs, traces, requests, jobs, audit entries — is backed by
ClickHouse and grows constantly. Offset pagination (?page=5) degrades badly there:
it has to count past everything it skips, and rows shifting underneath you cause
duplicates and gaps. Cursors encode a stable position in the result set, so page N
is cheap to fetch and the boundary between pages is exact regardless of what’s been
written since. Cursor pagination is used everywhere in the API, for consistency.
The parameters
Section titled “The parameters”| Parameter | Type | Description |
|---|---|---|
limit |
integer | Page size. Defaults to a sensible value per endpoint; capped at a per-endpoint maximum (100 on most list endpoints — see each resource page). |
cursor |
string | The opaque next_cursor from a previous response. Omit it on the first request. |
Every collection response carries a pagination block:
"pagination": { "next_cursor": "eyJ0…", "has_more": true}has_more—truewhen more pages exist. Loop until it’sfalse.next_cursor— pass ascursoron the next request. It’snullwhenhas_moreisfalse.
A worked loop
Section titled “A worked loop”Fetch every site by following next_cursor until has_more is false.
curl https://api.managed.dev/v1/sites?limit=2 \ -H "Authorization: Bearer mfk_live_…" \ -H "Forge-Version: 2026-06-23"curl "https://api.managed.dev/v1/sites?limit=2&cursor=eyJ0…" \ -H "Authorization: Bearer mfk_live_…" \ -H "Forge-Version: 2026-06-23"let cursor: string | undefined;const sites = [];
do { const page = await mf.sites.list({ limit: 100, cursor }); sites.push(...page.data); cursor = page.pagination.has_more ? page.pagination.next_cursor : undefined;} while (cursor);var cursor stringvar sites []forge.Site
for { page, err := client.Sites.List(ctx, &forge.SiteListParams{ Limit: 100, Cursor: cursor, }) if err != nil { return err } sites = append(sites, page.Data...) if !page.Pagination.HasMore { break } cursor = page.Pagination.NextCursor}Auto-pagination in the SDKs
Section titled “Auto-pagination in the SDKs”The official SDKs hide the loop behind an iterator, so you rarely write it by hand. Iterate and the SDK fetches pages lazily as you consume them:
for await (const site of mf.sites.listAll({ limit: 100 })) { console.log(site.id, site.runtime);}iter := client.Sites.ListAll(ctx, &forge.SiteListParams{Limit: 100})for iter.Next() { site := iter.Current() fmt.Println(site.ID, site.Runtime)}if err := iter.Err(); err != nil { return err}Stable ordering
Section titled “Stable ordering”Within a single cursor walk, ordering is stable: each endpoint sorts by a
deterministic key (typically created_at plus the id as a tiebreaker), and the
cursor pins your position against that order. Items created after you started
paginating may not appear until you start a fresh walk — which is the correct,
gap-free behavior for an append-heavy log. If you need only the newest rows, start a
new request rather than continuing an old cursor.