v2.2.0

CallListV2.make

Method for quickly retrieving all data from list methods of Bitrix24 REST API version 2.
Bitrix24 is gradually transitioning to REST API version 3.

Overview

When you need to retrieve all records from list methods of REST API version 2 with maximum efficiency and are ready to store the complete dataset in memory, use CallListV2.make().

This method automatically handles pagination and returns all data in a single array.

For importing, exporting, processing all elements and generating reports, we recommend using FetchList - it returns an asynchronous generator for step-by-step processing of large lists.
// Basic usage
import { EnumCrmEntityTypeId } from '@bitrix24/b24jssdk'

const response = await $b24.actions.v2.callList.make({
  method: 'crm.item.list',
  params: {
    entityTypeId:  EnumCrmEntityTypeId.deal,
    filter: { '>opportunity': 10000 }
  },
  idKey: 'id',
  customKeyForResult: 'items',
  requestId: 'unique-request-id'
})

When to Use CallListV2.make()

  1. Small to medium data volumes: When the total number of records does not exceed several thousand.
  2. Simple processing: When you need simple access to all data at once.
  3. Data aggregation: When you need to perform operations on the entire dataset (summation, grouping, etc.).

Method Signature

make<T = unknown>(
  options: ActionCallListV2
): Promise<Result<T[]>>

Parameters

The options object contains the following properties:

ParameterTypeRequiredDescription
methodstringYesREST API method name that returns a data list (e.g., crm.contact.list, tasks.task.list).
paramsOmit<TypeCallParamsV2, 'start' | 'order'>NoRequest parameters, excluding the start and order parameters. The start parameter is reserved because the method retrieves all data in a single call. The order parameter is reserved because cursor-based pagination requires sorting strictly by cursorIdKey (which defaults to idKey) ascending — see Limitations. Use filter and select to control the selection.
idKeystringNoName of the id field as it appears in each response item; its value drives the cursor. Default: 'ID' (uppercase). For methods that return a lowercase / camelCase id (e.g. tasks.task.list returns id), set 'id'.
cursorIdKeystringNoField name used in the request for order and the > page filter. Defaults to idKey. Set it only when the sortable / filterable field name differs from the response field name — e.g. tasks.task.list sorts and filters by ID but returns id: pass idKey: 'id', cursorIdKey: 'ID'.
customKeyForResultstringNoCustom key indicating that the REST API response will be selected by this field. For example: items for a list of CRM elements.
requestIdstringNoUnique request identifier for tracking and debugging — sent as the bx24_request_id query parameter. It does not deduplicate anything; for that see idempotencyKey (restApi:v3).
maxPagesnumberNoCeiling on how many pages the walk may read. Default 10000; must be a positive integer, or JSSDK_ACTION_INVALID_MAX_PAGES is raised at call time. Reaching it does not discard what was read — the collected rows come back with a JSSDK_ACTION_MAX_PAGES_EXCEEDED error attached, so check isSuccess — see Bounding the walk.
signalAbortSignalNoCancels the walk. Checked before each request, so an already-aborted signal costs no request at all. Cancelling is not losing: the pages already read come back with a JSSDK_ACTION_ABORTED error attached.
progress(p: { pages: number, rows: number }) => voidNoCalled after each collected page with cumulative counts. Counts rather than a percentage: cursor paging never asks the portal for a total, so a denominator would have to be invented.

Return Value

Promise<Result<T[]>> — a promise that resolves to a Result<T[]> object containing an array of all retrieved elements.

This object provides:

  • .getData(): T[] — returns an array of all retrieved elements.
  • .isSuccess: boolean — flag indicating successful execution of all requests.
  • .getErrorMessages(): string[] — array of error messages.

Key Concepts

Automatic Warning

When calling methods that are available in REST API version 3, the system automatically logs a warning:

"The method {method_name} is available in restApi:v3. It's worth migrating to the new API."

This indicates that you should consider migrating to the newer REST API version.

Performance Optimization

The method implements the Bitrix24 recommended algorithm for efficient work with large data volumes:

  1. start: -1: Disables counting the total number of records, significantly speeding up query execution.
  2. Filtering by increasing id: Each subsequent query uses a >cursorIdKey filter with the id of the last retrieved element (read via idKey).
  3. Automatic data end detection: Requests stop when an empty array is received or the number of elements is less than the page size (50). A short page ends the walk only once the cursor read from it has been checked — a page that is short and repeats the cursor is a server ignoring the page condition, not the end of the data, and is reported rather than returned.

REST API v2 Response Structure

In REST API version 2, various methods may return data in different structures:

  • Direct array: { result: [...] }
  • Grouped array: { result: { items: [...] } }

The customKeyForResult parameter allows you to specify the key where the data is located in the response.

Limitations

  • Page size: Fixed limitation of Bitrix24 REST API version 2 — 50 records per request.
  • Sorting is fixed: The method always sorts by cursorIdKey (which defaults to idKey) ascending, because cursor pagination relies on a >cursorIdKey filter to walk the dataset. A user-supplied order value would break that invariant, so the declared order property is Omitted from the type — though the [key: string]: unknown index signature it inherits means the compiler still accepts one — and any value passed at runtime is stripped with a warning log entry. To narrow the result set, use filter instead.
  • Conditions go in lowercase filter, and FILTER has to be removed: the method pages by writing its own lowercase filter, order and start, and the portal keeps only the later of two top-level keys that differ by case. Older list methods are documented with uppercase FILTER / SORT / ORDER, so following that documentation here is the mistake. Measured on user.get with four users: FILTER: { ID: 4 } returns all four, filter: { ID: 4 } returns one. Which of the pair is later is not fixed, so the two ways of getting this wrong fail in opposite directions — passing only FILTER drops your conditions, while passing FILTER and filter drops the walker's own cursor and stalls the walk (see the next bullet). Move the conditions across; do not just add a lowercase key beside the uppercase one. SORT fails louder still — on user.get it makes the injected order fail the method's own validation, so the request throws ERROR_ARGUMENT / "Order must be a string". All of these are reported with a warning.
  • A cursor that stops advancing is fatal: if the last idKey value on a page equals the one already filtered on, the >cursorIdKey condition was dropped and the same page will keep arriving. A full page cannot end the walk on its own — the length < 50 stop never fires — and a short one is checked before it is taken for the end of the data, so it throws SdkError with code JSSDK_ACTION_CURSOR_STALLED instead of collecting duplicates for ever. The usual cause is idKey naming the response field while cursorIdKey is left to default to it: tasks.task.list returns a lowercase id but filters on an uppercase ID, so it needs both (idKey: 'id', cursorIdKey: 'ID'). A cursor that moves the wrong way is caught too, and separately: a server alternating between two pages never repeats the value just sent, so the equality test above cannot see it — the walk is stopped with JSSDK_ACTION_CURSOR_WENT_BACKWARDS on the first backwards step. And neither check waits for a full page any more: a short page whose cursor did not advance used to end the walk as end-of-data, returning a truncated and overlapping result with no error at all.
  • Only for list methods: Intended only for methods that return data arrays.

Error Handling

Always check the result using isSuccess and handle errors:

// @check-ignore: top-level return in error-handling illustration

const response = await $b24.actions.v2.callList.make({
  method: 'some.method',
  params: { /* some_params */ },
  idKey: 'id',
  customKeyForResult: 'items',
  requestId: 'unique-request-id'
})

if (!response.isSuccess) {
  // Handling error
  console.error(new Error(`Error: ${response.getErrorMessages().join('; ')}`))
  return
}

// Working with a successful result
const data = response.getData()

Examples

Getting all companies with filtering

AllCrmItems.ts
import { B24Hook, EnumCrmEntityTypeId, LoggerFactory, Text, SdkError, AjaxError } from '@bitrix24/b24jssdk'

type Company = {
  id: number
  title: string
}

const devMode = !!(typeof import.meta !== 'undefined' && (import.meta.dev || import.meta.env?.DEV))
const $logger = LoggerFactory.createForBrowser('Example:AllCrmItems', devMode)
const $b24 = B24Hook.fromWebhookUrl('https://your_domain.bitrix24.com/rest/1/webhook_code/')

async function getCrmItemList(requestId: string): Promise<Company[]> {
  const sixMonthAgo = new Date()
  sixMonthAgo.setMonth((new Date()).getMonth() - 6)
  sixMonthAgo.setHours(0, 0, 0)

  const response = await $b24.actions.v2.callList.make<Company>({
    method: 'crm.item.list',
    params: {
      entityTypeId: EnumCrmEntityTypeId.company,
      filter: {
        // use some filter by title
        '=%title': 'Prime%',
        '>=createdTime': Text.toB24Format(sixMonthAgo) // created at least 6 months ago
      },
      select: ['id', 'title']
    },
    idKey: 'id',
    customKeyForResult: 'items',
    requestId
  })

  if (!response.isSuccess) {
    throw new SdkError({
      code: 'MY_APP_GET_PROBLEM',
      description: `Problem ${response.getErrorMessages().join('; ')}`,
      status: 404
    })
  }

  return response.getData() ?? []
}

// Usage
const requestId = 'some-crm-item-list'
try {
  const list = await getCrmItemList(requestId)
  $logger.info(`List [${list?.length}]`, { requestId, list })
} catch (error) {
  if (error instanceof AjaxError) {
    $logger.critical(error.message, { requestId, code: error.code })
  } else {
    $logger.alert('Problem', { requestId, error })
  }
}

Fetching all tasks.task.list records (request field ≠ response field)

tasks.task.list sorts and filters by ID (uppercase) but returns each task with a lowercase id. Set idKey to the response field and cursorIdKey to the request field:

AllTasks.ts
type TaskListItem = { id: string, title: string }

const response = await $b24.actions.v2.callList.make<TaskListItem>({
  method: 'tasks.task.list',
  params: {
    filter: { RESPONSIBLE_ID: 1 }, // tasks filter fields are uppercase
    select: ['ID', 'TITLE'] // selected uppercase, but the response carries lowercase id / title
  },
  idKey: 'id', // read task.id from the response
  cursorIdKey: 'ID', // order + ">ID" page filter in the request
  customKeyForResult: 'tasks'
})

console.log(`Loaded ${response.getData()?.length ?? 0} tasks`)

Without cursorIdKey, the default idKey: 'ID' can't be read from the lowercase response, so pagination stops after the first 50 tasks — and the SDK logs a warning that points you here.

Alternatives and Recommendations

  • For working with lists: Instead of manually managing pagination use:
    • FetchList — returns an async generator for step-by-step processing of large lists.
  • For batch operations: Use Batch to execute up to 50 commands in a single request.
  • On the client-side (browser): Use the built-in B24Frame object.
  • See also: Filtering — building the filter (and why order is stripped).

Bounding the walk

The walk pages until the data runs out. Three options bound it.

maxPages stops after that many pages and throws JSSDK_ACTION_MAX_PAGES_EXCEEDED, naming the method. The rows already read are not discarded — they come back with the error attached, so check isSuccess. It defaults to 10 000 — a backstop rather than a policy: on restApi:v2 that is 500 000 rows, and at the default drain rate of 2 requests/second roughly 83 minutes, so a walk that never ends is bounded without capping a read anyone performs. Raise it when a read genuinely needs more.

It errors rather than truncating. A short list that looks complete is the failure mode this whole class of bug keeps producing, so nothing is returned.

signal takes an AbortSignal and throws JSSDK_ACTION_ABORTED. It is checked at the top of each iteration, so an already-aborted signal costs no request at all.

progress is called after each collected page with { pages, rows }. Counts, not a percentage: cursor paging reads no total — restApi:v3 sends none, and on v2 the walk never asks for one — so a denominator would have to be invented.

// const $b24 = ...
const controller = new AbortController()

const response = await $b24.actions.v2.callList.make({
  method: 'crm.item.list',
  params: { entityTypeId: 4 },
  idKey: 'id',
  customKeyForResult: 'items',
  maxPages: 200,
  signal: controller.signal,
  progress: ({ pages, rows }) => console.log(`${pages} pages, ${rows} rows`)
})