Pagination
Paginated lists, including domains and webhooks, use cursor pagination by default. Walk forward with `after` or backward with `before`. Send `page` to opt into offset pagination where it is enabled.
Two modes
sevk's paginated endpoints use two response shapes. Omitting page selects cursor mode, even when no cursor is supplied:
- Offset (
?page=N&limit=N): jump to any page, see the total. Good for dashboards with numbered page navigation. - Cursor (
?after=<id>&limit=Nor?before=<id>&limit=N): walk forward or backward from where you left off. Stable when the underlying data changes mid-iteration; required for tables large enough that deep pages slow down.
Sending page together with a cursor returns 400. Sending both after and before in the same request also returns 400. The response shape varies by mode (see below).
limit is the same in both modes: 1 to 100, default 20.
Offset mode
Pass ?page=N (1-indexed). sevk skips (page - 1) * limit rows and returns the next limit rows along with the total count.
Bounded lists such as domains, webhooks, audiences, templates, and broadcasts accept offset pagination. High-volume lists such as contacts, activity, inbound emails, and broadcast emails require the operator to enable PAGINATION_OFFSET_ENABLED; it is disabled by default. Use cursor pagination for code that must work with either setting.
Request
const page1 = await sevk.domains.list({ page: 1, limit: 50 })const page2 = await sevk.domains.list({ page: 2, limit: 50 })console.log(page1.items, page1.total, page1.page, page1.totalPages)
Response
| Name | Type | Description |
|---|---|---|
items | T[] | Rows for the current page. |
total | number | Total rows matching the filter across all pages. |
page | number | Current page number. |
totalPages | number | Total page count. |
Two things to know:
- Cost grows with page number, and there is a hard ceiling. The offset (
(page - 1) * limit) is capped byPAGINATION_MAX_OFFSET, which defaults to 50,000 rows. A request past that ceiling returns 400. Use cursor mode for deeper pages. - Page boundaries shift on writes. If a row is added or removed between two list calls, an offset request can return the same row on consecutive pages, or skip a row entirely. Cursor pagination avoids this offset shift, but does not provide a snapshot of changing data.
Cursor mode
Omit page, after, and before to fetch the first cursor page. Empty ?after= or ?before= values also select the first page. To continue, pass ?after=<id> with nextCursor while hasMore is true. To walk backward, pass ?before=<id> with prevCursor. Each response includes both cursors and a hasMore flag for the requested direction.
Replace process in the following examples with your own data handler.
Request
let page = await sevk.contacts.list({ limit: 50 })process(page.items)while (page.hasMore && page.nextCursor) {page = await sevk.contacts.list({ after: page.nextCursor, limit: 50 })process(page.items)}
Response
| Name | Type | Description |
|---|---|---|
items | T[] | Rows in the endpoint’s normal sort order, regardless of `after` vs `before`. |
hasMore | boolean | True when at least one more page is available in the direction you requested. |
nextCursor | string | null | Id of the last row in the current page. Pass back as `after` to continue forward (older rows). `null` when the page is empty. |
prevCursor | string | null | Id of the first row in the current page. Pass back as `before` to walk backward (newer rows). `null` when the page is empty. Always populated alongside `nextCursor` so navigation in either direction stays a single round-trip. |
How the cursor resolves
The cursor is the id of a row. sevk looks up that row's sort value, then orders by (sort value DESC, id DESC). Most endpoints sort by a timestamp; inbound routes sort by priority. after continues past the cursor and before walks in the other direction. The id breaks ties, and the cursor row itself is always excluded.
If the cursor row no longer exists, the request returns 400 (cursor must reference an existing row in the project).
Walking the full list
// example: pull every contact in the projectasync function exportAllContacts() {let cursor: string = ''while (true) {const page = await sevk.contacts.list({ after: cursor, limit: 100 })for (const contact of page.items) {process(contact)}if (!page.hasMore || !page.nextCursor) breakcursor = page.nextCursor}}
Walking backward
Use before with prevCursor from the current page to fetch rows newer than what you have. Common case: a live feed where you keep prevCursor around and poll for any items that arrived since.
// use prevCursor (the id of the first row on the current page) to walk the other wayconst current = await sevk.contacts.list({ after: '', limit: 50 })// rows newer than the current batch, still ordered newest firstif (current.prevCursor) {const previous = await sevk.contacts.list({ before: current.prevCursor, limit: 50 })}
cURL
# offsetcurl 'https://api.sevk.io/domains?page=2&limit=50' \-H 'Authorization: Bearer sevk_xxxxxxxxx'# cursor forward (use the id of the last item from the previous response)curl 'https://api.sevk.io/contacts?after=b2c3d4e5-f678-4901-abcd-ef1234567890&limit=50' \-H 'Authorization: Bearer sevk_xxxxxxxxx'# cursor backward (use prevCursor from the previous response)curl 'https://api.sevk.io/contacts?before=b2c3d4e5-f678-4901-abcd-ef1234567890&limit=50' \-H 'Authorization: Bearer sevk_xxxxxxxxx'
Which one to use
| Name | Type | Description |
|---|---|---|
Dashboard with page numbers | offset | You need to render "Page 5 of 23" or jump to page 50. Cursor cannot do this without walking the chain. |
Bulk export / sync job | cursor | Walk the list without deep offset queries or shifted page boundaries. This is not a snapshot of concurrently changing data. |
Live feed (activity, broadcast emails) | cursor | New rows arrive constantly. Offset shifts the boundaries; cursor stays anchored to the row you last saw. |
Small admin lists (templates, domains) | either | For under a few thousand rows the difference is negligible. Pick whichever the consumer finds easier to wire up. |
Errors
| Name | Type | Description |
|---|---|---|
400 Bad Request | page + cursor | Both `page` and a cursor (`after` or `before`) were sent. Pick one. |
400 Bad Request | after + before | Both `after` and `before` were sent. They are mutually exclusive; pick a direction. |
400 Bad Request | unknown cursor | The id passed as `after` or `before` does not exist in this project (or was deleted). Restart from the beginning or skip back to a known id. |
400 Bad Request | offset disabled | This endpoint requires PAGINATION_OFFSET_ENABLED for page-based requests. Omit page and use cursor pagination. |
400 Bad Request | offset too large | The requested offset ((page - 1) * limit) exceeds PAGINATION_MAX_OFFSET, which defaults to 50,000 rows. Use cursor pagination for deeper pages. |