Pagination

Every list that can grow without bound is cursor-paginated: conversations, messages, the audit trail and an endpoint's deliveries. You ask for a page, get the items and an opaque cursor, and pass the cursor back for the next page.

How cursors work#

A list request takes limit (1 to 100, default 50; larger values are clamped) and cursor. The response carries the items under the resource name and next_cursor, which is null on the last page. The cursor is opaque and stable under concurrent writes, so a walk that started before new rows arrived still finishes cleanly.

One page of conversations
{
  "ok": true,
  "data": {
    "conversations": [
      {
        "id": "CONV#acme#instagram#17841405309211844#6021573449812077",
        "platform": "instagram",
        "account_handle": "acme.ink",
        "participant_handle": "sam.draws",
        "last_message_text": "Do you have anything on Saturday?",
        "window": { "state": "open_24h", "expires_at": "2026-09-04T03:14:22Z" },
        "unanswered": true,
        "can_reply": true,
        "can_send_as_human_agent": true
      }
    ],
    "next_cursor": "eyJwayI6IkNPTlYjYWNtZSNpbnN0YWdyYW0jMSMyIn0"
  }
}

Walk pages by hand#

The loop is the same in every language: request, handle the items, stop when there is no cursor. A small limit here makes the paging visible; use 100 in real code.

TypeScript
import { HookChat } from '@hookchat/node'

const client = new HookChat({ apiKey: process.env.HOOKCHAT_API_KEY!, baseUrl: process.env.HOOKCHAT_BASE_URL! })

let cursor: string | undefined
let total = 0
let pages = 0
do {
  const page = await client.conversations.list({ limit: 2, cursor })
  total += page.conversations.length
  pages += 1
  cursor = page.next_cursor ?? undefined
} while (cursor)
console.log(total, 'conversations over', pages, 'pages')

Let the SDK iterate#

Each paginated resource has an iterator that follows the cursor for you and yields one item at a time. Pages are fetched lazily, so breaking out of the loop stops the requests, and a maximum item count bounds the walk.

TypeScript
import { HookChat } from '@hookchat/node'

const client = new HookChat({ apiKey: process.env.HOOKCHAT_API_KEY!, baseUrl: process.env.HOOKCHAT_BASE_URL! })

let count = 0
for await (const conversation of client.conversations.iterate({ limit: 50, maxItems: 10 })) {
  console.log(conversation.id, conversation.window.state, conversation.unanswered ? 'unanswered' : '')
  count += 1
}
console.log(count, 'conversations')

Filter the message feed#

The tenant-wide message feed is newest first and takes two filters: conversation_id narrows it to one thread and direction to inbound or outbound. A filtered page can be short and still carry a cursor, so always loop on the cursor rather than on the page size. Rows written by test-scope keys appear only with include_test.

TypeScript
import { HookChat } from '@hookchat/node'

const client = new HookChat({ apiKey: process.env.HOOKCHAT_API_KEY!, baseUrl: process.env.HOOKCHAT_BASE_URL! })

const page = await client.messages.list({
  conversation_id: process.env.CONVERSATION_ID!,
  direction: 'inbound',
  limit: 5,
})
for (const message of page.messages) console.log(message.id, message.direction, message.sent_at, message.text)
console.log(page.messages.length, 'inbound messages')

Limits and the unpaginated lists#

Next#