Skip to content
ChatfuelSDK
ReferenceChatfuel API

Pagination

Relay cursors with a scalar and a PageInfo type per connection, the direction flip on message history, where first is required, and the connections that accept pagination arguments and ignore them.

Connections are Relay-shaped, but almost nothing about them is shared — there are fifteen cursor scalars and fourteen PageInfo types, one set per connection.

type MessagePage {
  edges: [MessageEdge!]!
  pageInfo: MessagePageInfo!
}

type MessageEdge {
  node: Message!
  cursor: MessagesCursor!
}

type MessagePageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: MessagesCursor
  endCursor: MessagesCursor
}

The scalars are MessagesCursor, ContactSearchCursor, BotsCursor, BotAttributeCursor, WhatsAppTemplateCursor, KeywordRuleCursor, CommentReplyRulesCursor, CommentReplyRuleV2Cursor, FuelyBroadcastCursor, MetaAdCursor, InstagramMediasCursor, CoworkerConversationsCursor, CoworkerMessagesCursor, FbPageCursor and FbPagePostCursor. All of them are opaque strings: echo the values you get back from pageInfo and edges, and never build one. One PageInfo type is called exactly that, which reads like the shared one and is not — it belongs to BotsConnectionV2, and its cursors are BotsCursor.

Where first is required

messages(first: Int, after: MessagesCursor, before: MessagesCursor): MessagePage!
contactsConnection(platforms: [Platform!]!, first: Int!, before: ContactSearchCursor, after: ContactSearchCursor, segment: SegmentInput, orderBy: ContactSearchOrderByInput): ContactConnection!
contactDealsConnection(first: Int!, before: ContactSearchCursor, after: ContactSearchCursor, assigneeFilter: ContactAssigneeFilter!, salesStageV2Filter: SalesStageV2!): ContactConnection!
contactChatsConnection(first: Int!, before: ContactSearchCursor, after: ContactSearchCursor, assigneeFilter: ContactAssigneeFilter!, unreadOnly: Boolean!, salesStageV2Filter: [SalesStageV2!]!, textInputFilter: String): ContactConnection!

first is optional on Conversation.messages and required on the three contact-ish connections. It varies per field, so read the signature in the bundled schema instead of assuming.

Message history runs backwards

ArgumentsOrder you getWhat paging forward means
No cursor, or afterDescending — newest message first.after: pageInfo.endCursor walks backwards into history, one older page at a time.
beforeAscending.Forward in time.

A chat UI loads messages(first: 100), then answers "scroll up for older" with messages(first: 100, after: <endCursor>). pageInfo.startCursor is the newest message's cursor, which is what a mark-as-read call wants.

Connections that take pagination arguments and ignore them

FieldWhat it actually does
Bot.whatsAppTemplates(first, after, before)Accepts all three and ignores them: it returns the full list every time. The field is also nullable.
Bot.goodsCatalog, Bot.specialists, Bot.contactScopes, Bot.members, Bot.invites, Bot.bookingsV2, Workspace.bots, currentUser.workspacesNo pagination arguments at all: each returns its complete result every time. bookingsV2 takes a start and an end time, but nothing to page with.

Accumulating pages alongside live updates

The chat, contact and deal lists are forward-only in practice: page with after: pageInfo.endCursor, guard on hasNextPage, and never start a page fetch while the previous one is still in flight.

RuleWhy
De-duplicate by node id, replacing rather than appending.An incoming page or update can contain nodes you already hold.
Re-sort yourself.contactsChatUpdates and contactsDealUpdates tell you what changed, not where it belongs — sort the chat list by lastConversationMessageTime descending, deals by lastSalesStageUpdateTime descending.
Merge messages by clientId, never by id.Message.id is nullable; clientId is the reliable key.
Treat an invalid-cursor error as "start again".An after cursor that has fallen out of the result window stops resolving; refetch from the first page.

Changing any filter argument resets the connection. The cursors you hold belong to the old argument set, so the accumulated edges have to be thrown away — keep them and the list quietly becomes two different result sets stacked on top of each other.

On this page