v2.2.0

BatchV2.make

Method for executing batch requests to Bitrix24 REST API version 2. Allows executing up to 50 commands in a single API call.

Overview

Use BatchV2.make() to execute up to 50 REST API commands in a single request. This is especially useful when you need to retrieve or update large amounts of data while minimizing network requests and adhering to REST API limits.

// Basic usage
import { EnumCrmEntityTypeId } from '@bitrix24/b24jssdk'

const response = await $b24.actions.v2.batch.make({
  calls: [
    ['crm.item.get', { entityTypeId: EnumCrmEntityTypeId.contact, id: 1 }],
    ['crm.item.get', { entityTypeId: EnumCrmEntityTypeId.contact, id: 2 }]
  ],
  options: {
    isHaltOnError: true,
    returnAjaxResult: true,
    requestId: 'unique-request-id'
  }
})

Method Signature

make<T = unknown>(
  options: ActionBatchV2
): Promise<CallBatchResult<T>>

Parameters

The options object contains the following properties:

ParameterTypeRequiredDescription
callsBatchCommandsArrayUniversal | BatchCommandsObjectUniversal | BatchNamedCommandsUniversalYesCommands to execute in the batch. Supports several formats.
optionsIB24BatchOptionsNoAdditional options for batch request execution.

Command Formats (options.calls)

1. Array of tuples (BatchCommandsArrayUniversal)

// @check-ignore: partial snippet — calls array literal, not a valid statement

calls: [
  ['method1', params1],
  ['method2', params2],
  // ...
]

2. Array of objects (BatchCommandsObjectUniversal)

// @check-ignore: partial snippet — object array literal, not a valid statement

calls: [
  { method: 'method1', params: params1 },
  { method: 'method2', params: params2 },
  // ...
]

3. Object with named commands (BatchNamedCommandsUniversal)

calls: {
  command1: { method: 'method1', params: params1 },
  command2: ['method2', params2],
  // ...
}

Batch Request Options (options.options)

OptionTypeDefaultDescription
isHaltOnErrorbooleantrueWhether to stop execution on the first error.
returnAjaxResultbooleanfalseWhether to return an AjaxResult object instead of data.
requestIdstringUnique 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).
These belong insideoptions. Passed as top-level properties of the argument they are not applied: TypeScript rejects the literal, and a caller the compiler never sees — plain JavaScript, or a literal widened through a variable — gets a logged warning naming the flag and where it goes. Before that (#426) it was silent, and returnAjaxResult dropped this way is the one that bites: isSuccess on a plain payload is undefined, so a batch where every command succeeded reads as a batch where every command failed.

Return Value

The shape is chosen from the arguments, and the overloads say which you get — no cast, no narrowing by hand:

callsoptions.returnAjaxResultresolves to
named commands (a record)absent / falseResult<Record<string, T>> — the payload per command name
named commands (a record)trueResult<Record<string, AjaxResult<T>>>
array of commands (tuples or objects)absent / falseResult<T[]> — the payload per position
array of commands (tuples or objects)trueResult<AjaxResult<T>[]>

T is one command's payload in every row — not the collection.

A command's arguments go in params. Write them under query and they are read by nobody: a v2 command goes on the wire as method?<querystring> built from params, so it is sent with no arguments at all and the portal answers HTTP 200 with the method's defaults.Measured on user.get with filter: { ACTIVE: 'N' } against a portal whose only user is active: under params the portal answered 0 rows — the filter did its work — and the same request spelled query answered 1 row, the active user the filter was meant to exclude. A wrong answer, not a failure.query is the restApi:v3 wire spelling, which is why it is the one a caller reaches for; on v2 it is simply a key the parser does not read.TypeScript rejects it in a fresh object literal. It does not see the same literal assigned to a variable first, or commands built from a config object, a JSON.parse, or plain JavaScript — so the SDK also warns at run time: one line per batch request, naming the keys it ignored and the commands that carried them. It warns only where the arguments were actually lost — a command with no params, or one naming query. Your own id or label beside a populated params is left alone.
An array of command objects ([{ method, params }]) answers by index, like the tuple form. It is the record form ({ name: { method, params } }) that answers by name — the dispatch is on the container, not on what the entries look like.
When returnAjaxResult is a boolean the compiler cannot read as a literal — a variable rather than true or false written in place — no overload can decide for you, and the call resolves to the union CallBatchResult<T>. That is honest rather than helpful: narrow it yourself, or pass the flag as a literal.

Result Item Data

Each per-command result is returned exactly as the REST API delivered it, including null when the underlying method legitimately returns no data (for example, im.chat.get called with an ENTITY_TYPE that does not match any chat).

const response = await $b24.actions.v2.batch.make<{ ID: number } | null>({
  calls: {
    chatGet: {
      method: 'im.chat.get',
      params: { ENTITY_TYPE: 'UNKNOWN', ENTITY_ID: 'UNKNOWN' }
    }
  },
  options: { returnAjaxResult: true, requestId: 'chat-get' }
})

// No cast and no `@check-ignore`: named commands plus `returnAjaxResult: true`
// resolve to a record of `AjaxResult`, and the overloads read that off the
// arguments (#518).
const chatGet = response.getData()!.chatGet!
if (chatGet.getData()?.result === null) {
  // method returned null — no chat matched
} else {
  console.log(chatGet.getData()?.result.ID)
}
Prior to v1.1.1, an null result was coerced to {}, which broke nullable type guards on the caller side (see issue #23). When typing the generic for methods that may return null, declare it as T | null.

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.batch.make({
  calls: [
    { method: 'crm.item.get', params: { entityTypeId: EnumCrmEntityTypeId.contact, id: 1 } },
    { method: 'crm.item.get', params: { entityTypeId: EnumCrmEntityTypeId.contact, id: 2 } }
  ],
  options: {
    isHaltOnError: true,
    returnAjaxResult: true,
    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()

The isSuccess check covers two distinct failures:

  • Per-command errors — the batch envelope succeeds but individual commands fail. Inspect each AjaxResult in getData(), or use getErrorsByKey() to see which command failed.
  • Envelope-level (soft) errors — the whole batch fails before any command result is returned, e.g. a server-side validation code (BITRIX_REST_V3_EXCEPTION_VALIDATION_*). The outer Result is then isSuccess === false, the error(s) are in getErrorMessages() / getErrors(), and getData().result is empty. When that code is a validation one, the AjaxError also names the field that failed — see Which field failed.

A single if (!response.isSuccess) guard handles both cases.

Examples

Getting Multiple CRM Companies

import type { AjaxResult, Result, BatchCommandsArrayUniversal } from '@bitrix24/b24jssdk'
import { B24Hook, EnumCrmEntityTypeId, LoggerFactory, SdkError, AjaxError } from '@bitrix24/b24jssdk'

type Company = {
  id: number
  title: string
  [key: string]: any
}

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

async function getMultipleItems(itemIds: number[], requestId: string): Promise<Company[]> {
  if (itemIds.length < 1 || itemIds.length > 50) {
    throw new SdkError({
      code: 'MY_APP_GET_PROBLEM',
      description: `The number of elements must be between 1 and 50`,
      status: 404
    })
  }

  const batchCalls: BatchCommandsArrayUniversal = itemIds.map(id => [
    'crm.item.get',
    {
      entityTypeId: EnumCrmEntityTypeId.company,
      id
    }
  ])

  const response = await $b24.actions.v2.batch.make<{ item: Company }>({
    calls: batchCalls,
    options: {
      isHaltOnError: true,
      returnAjaxResult: true,
      requestId
    }
  })

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

  const resultData = (response as Result<AjaxResult<{ item: Company }>[]>).getData()!
  const results: Company[] = []
  resultData.forEach((resultRow, _index) => {
    if (resultRow.isSuccess) {
      results.push((resultRow.getData()!.result as { item: Company }).item)
    }
  })

  return results
}

// Usage
const requestId = 'batch/crm.item.get'
try {
  const itemIds = [2, 4]
  const items = await getMultipleItems(itemIds, requestId)

  $logger.info(`Retrieved ${items.length} items`, {
    expected: itemIds.length,
    retrieved: items.length,
    items: items.map(c => ({ id: c.id, title: c.title }))
  })
} catch (error) {
  if (error instanceof AjaxError) {
    $logger.critical(error.message, { requestId, code: error.code })
  } else {
    $logger.alert('Problem', { requestId, error })
  }
}

Init Data Storage

This code automates the creation and initialization of data storages in Bitrix24 via the REST API.

It checks the existence of the specified storages and, if they don't exist, creates them along with the specified properties, using batch requests for efficiency.

batch-rest-api-ver2-data-storage.ts
import type { BatchNamedCommandsUniversal } from '@bitrix24/b24jssdk'
import { B24Hook, LoggerFactory, SdkError, AjaxError } from '@bitrix24/b24jssdk'

type DataStorageParams = {
  ENTITY: string
  NAME: string
  ACCESS: Record<string, 'R' | 'W' | 'X'>
}

type PropertyParams = {
  PROPERTY: string
  NAME: string
  TYPE: 'S' | 'N' | 'F'
}

type DataStorage = {
  isInit: boolean
  dataStorage: DataStorageParams
  props: PropertyParams[]
}

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

async function initDataStorageList(dataStorageMap: Map<string, DataStorage>, requestId: string): Promise<void> {
  // get current list
  const response = await $b24.actions.v2.call.make<{ ENTITY: string, NAME: string }[]>({
    method: 'entity.get',
    params: {},
    requestId: `${requestId}/init:getCurrentList`
  })

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

  const currentDataStorageList = response.getData()!.result as { ENTITY: string, NAME: string }[]

  for (const dataStorage of dataStorageMap.values()) {
    const isInit = currentDataStorageList.some(row => row.ENTITY === dataStorage.dataStorage.ENTITY)
    if (isInit) {
      dataStorage.isInit = true
    } else {
      await initDataStorage(dataStorage, requestId)
      dataStorage.isInit = true
    }
  }
}

async function initDataStorage(dataStorage: DataStorage, requestId: string): Promise<void> {
  const callBatch: BatchNamedCommandsUniversal = {
    AddEntity: {
      method: 'entity.add',
      params: dataStorage.dataStorage
    }
  }

  for (const property of dataStorage.props) {
    callBatch[`prop${property.PROPERTY}`] = {
      method: 'entity.item.property.add',
      params: {
        ...property,
        ENTITY: dataStorage.dataStorage.ENTITY
      }
    }
  }

  const response = await $b24.actions.v2.batch.make({
    calls: callBatch,
    options: {
      isHaltOnError: true,
      returnAjaxResult: false,
      requestId: `${requestId}/init:${dataStorage.dataStorage.ENTITY}`
    }
  })

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

// Usage
const requestId = 'batch/DataStorage'
try {
  const dataStorageMap: Map<string, DataStorage> = new Map([
    ['DataStorage1', {
      isInit: false,
      dataStorage: {
        ENTITY: 'DS1',
        NAME: 'Data Storage 1',
        ACCESS: {
          U1: 'X',
          AU: 'R'
        }
      },
      props: [
        {
          PROPERTY: 'PropertyN',
          NAME: 'Property N',
          TYPE: 'N'
        }
      ]
    }],
    ['DataStorage2', {
      isInit: false,
      dataStorage: {
        ENTITY: 'DS2',
        NAME: 'Data Storage 2',
        ACCESS: {
          U1: 'X',
          AU: 'R'
        }
      },
      props: [
        {
          PROPERTY: 'PropertyS',
          NAME: 'Property S',
          TYPE: 'S'
        }
      ]
    }],
    ['DataStorage3', {
      isInit: false,
      dataStorage: {
        ENTITY: 'DS3',
        NAME: 'Data Storage 3',
        ACCESS: {
          U1: 'X',
          AU: 'R'
        }
      },
      props: [
        {
          PROPERTY: 'PropertyF',
          NAME: 'Property F',
          TYPE: 'F'
        }
      ]
    }]
  ])

  await initDataStorageList(dataStorageMap, requestId)

  $logger.info(`dataStorageList`, {
    items: [...dataStorageMap.values()].map(c => ({
      entity: c.dataStorage.ENTITY,
      isInit: c.isInit,
      title: c.dataStorage.NAME,
      props: c.props
    }))
  })
} catch (error) {
  if (error instanceof AjaxError) {
    $logger.critical(error.message, { requestId, code: error.code })
  } else {
    $logger.alert('Problem', { requestId, error })
  }
}

Delete Data Storage

This code deletes multiple data storages in the Bitrix24 system via the REST API.

It sequentially sends delete requests for each data storage specified in the dataStorageMap using the entity.delete method.

batch-rest-api-ver2-data-storage-delete.ts
import { B24Hook, LoggerFactory, SdkError, AjaxError } from '@bitrix24/b24jssdk'

type DataStorageParams = {
  ENTITY: string
  NAME: string
}

type DataStorage = {
  dataStorage: DataStorageParams
}

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

async function removeDataStorageList(dataStorageMap: Map<string, DataStorage>, requestId: string): Promise<void> {
  for (const dataStorage of dataStorageMap.values()) {
    const response = await $b24.actions.v2.call.make({
      method: 'entity.delete',
      params: {
        ENTITY: dataStorage.dataStorage.ENTITY
      },
      requestId: `${requestId}/init:getCurrentList`
    })

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

// Usage
const requestId = 'batch/DataStorageRemove'
try {
  const dataStorageMap: Map<string, DataStorage> = new Map([
    ['DataStorage1', {
      dataStorage: {
        ENTITY: 'DS1',
        NAME: 'Data Storage 1'
      }
    }],
    ['DataStorage2', {
      dataStorage: {
        ENTITY: 'DS2',
        NAME: 'Data Storage 2'
      }
    }],
    ['DataStorage3', {
      dataStorage: {
        ENTITY: 'DS3',
        NAME: 'Data Storage 3'
      }
    }]
  ])

  await removeDataStorageList(dataStorageMap, requestId)
} catch (error) {
  if (error instanceof AjaxError) {
    $logger.critical(error.message, { requestId, code: error.code })
  } else {
    $logger.alert('Problem', { requestId, error })
  }
}

Alternatives and Recommendations

  • For sequential requests: Use Call for single calls.
  • For working with lists: Use CallList for retrieving large volumes of data.
  • For step-by-step processing: Use FetchList for processing data as it arrives.
  • To run more commands (more than 50): Use BatchByChunk, which automatically splits commands into chunks of 50.
  • On the client-side (browser): Use the built-in B24Frame object.