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 message feed for the whole workspace is newest first and takes two filters: conversation_id narrows it to one thread and direction to inbound, outbound or echo (a message the account sent from the Instagram or Messenger app, which also reaches your endpoint as message.sent). 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.

Each message carries attachments, a list of { type, url } that is empty when there is no media. type is the kind Meta names, such as image, video, audio, file, share or story_mention. url is Meta's link to the media, or null. It expires after a while and HookChat does not keep a copy, so fetch the media soon if you want to keep it.

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#

  • limit is 1 to 100. The server clamps larger values to 100 rather than rejecting them.
  • Paginated: conversations, messages, audit entries, and an endpoint's deliveries. Each has an iterator in every SDK and --all in the CLI.
  • Not paginated: accounts, keys and webhook endpoints. They return the whole list.
  • The SDK iterators raise an error if a cursor does not advance, so a server defect cannot loop forever.

A cursor is only good for the list that handed it out. One from another list, another workspace or a bad copy is refused with 400 invalid_request and the message "The cursor is not one this list gave you"; start again without a cursor.

Next#