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=N or ?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

NameTypeDescription
itemsT[]Rows for the current page.
totalnumberTotal rows matching the filter across all pages.
pagenumberCurrent page number.
totalPagesnumberTotal page count.

Two things to know:

  • Cost grows with page number, and there is a hard ceiling. The offset ((page - 1) * limit) is capped by PAGINATION_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

NameTypeDescription
itemsT[]Rows in the endpoint’s normal sort order, regardless of `after` vs `before`.
hasMorebooleanTrue when at least one more page is available in the direction you requested.
nextCursorstring | nullId of the last row in the current page. Pass back as `after` to continue forward (older rows). `null` when the page is empty.
prevCursorstring | nullId 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 project
async 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) break
cursor = 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 way
const current = await sevk.contacts.list({ after: '', limit: 50 })
// rows newer than the current batch, still ordered newest first
if (current.prevCursor) {
const previous = await sevk.contacts.list({ before: current.prevCursor, limit: 50 })
}

cURL

# offset
curl '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

NameTypeDescription
Dashboard with page numbersoffsetYou need to render "Page 5 of 23" or jump to page 50. Cursor cannot do this without walking the chain.
Bulk export / sync jobcursorWalk the list without deep offset queries or shifted page boundaries. This is not a snapshot of concurrently changing data.
Live feed (activity, broadcast emails)cursorNew rows arrive constantly. Offset shifts the boundaries; cursor stays anchored to the row you last saw.
Small admin lists (templates, domains)eitherFor under a few thousand rows the difference is negligible. Pick whichever the consumer finds easier to wire up.

Errors

NameTypeDescription
400 Bad Requestpage + cursorBoth `page` and a cursor (`after` or `before`) were sent. Pick one.
400 Bad Requestafter + beforeBoth `after` and `before` were sent. They are mutually exclusive; pick a direction.
400 Bad Requestunknown cursorThe 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 Requestoffset disabledThis endpoint requires PAGINATION_OFFSET_ENABLED for page-based requests. Omit page and use cursor pagination.
400 Bad Requestoffset too largeThe requested offset ((page - 1) * limit) exceeds PAGINATION_MAX_OFFSET, which defaults to 50,000 rows. Use cursor pagination for deeper pages.