v2.2.0

BatchV3.make

Method for executing batch requests to Bitrix24 REST API version 3. The SDK sends up to 50 commands in a single API call.

Overview

Use BatchV3.make() to execute up to 50 REST API commands in a single request, minimizing network round-trips.

The 50 is the SDK's ceiling, not the portal's. 51 commands posted straight at the v3 batch endpoint answered HTTP 200 with 51 results, measured on an on-premise build — so the real v3 boundary is above 51 and is not published. The SDK holds v3 to the number restApi:v2 enforces (CRestUtil::BATCH_MAX_LENGTH = 50, read in the portal sources), which keeps chunking identical across versions and keeps an over-length batch a named client-side error rather than an unknown server-side one. Over 50, use BatchByChunk.
// Basic usage
import { EnumCrmEntityTypeId } from '@bitrix24/b24jssdk'

const response = await $b24.actions.v3.batch.make({
  calls: [
    ['tasks.task.get', { id: 1 }],
    ['tasks.task.get', { id: 2 }]
  ],
  options: {
    isHaltOnError: true,
    returnAjaxResult: true,
    requestId: 'unique-request-id'
  }
})

Method Signature

make<T = unknown>(
  options: ActionBatchV3
): 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],
  // ...
}
A command's arguments go in params; the portal's wire name is query. A v3 batch item goes on the wire as { method, query, as, parallel } — which is what the portal's own reference and every curl example show — and the SDK translates params into query for you. Write query in a command and the arguments are read by nobody: the command goes out with an empty query, which the portal accepts.Measured on main.eventlog.list with select: ['id'] and pagination: { limit: 2 }: under params the portal answered 2 rows of 1 field; the same request spelled query answered 50 rows of 13 fields — the whole default page, with the select and the limit both gone. HTTP 200 either way, no error anywhere. A filter written that way is a wrong answer, not a failure, so there is nothing to notice.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.

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.

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. When typing the generic for methods that may return null, declare it as T | null.

Prior to v1.1.1, a null result was coerced to {}, which broke nullable type guards on the caller side (see issue #23).

Limitations

restApi:v3 batch is all-or-nothing:

  • Per-command errors are not returned. If any command in the batch fails, the whole batch fails and the top-level response.getErrorMessages() contains the error(s); getData() returns an empty map.
  • time on each AjaxResult is the batch-level time, not per-command. The rate-limiter does not attribute the batch duration to individual methods.
  • If a successful v3 response is missing a result entry for a command (which indicates a malformed API response), the SDK throws JSSDK_INTERACTION_BATCH_STRATEGY_V3_EMPTY_COMMAND_RESPONSE.
A restApi:v3 batch does not work from the browser. The commands are the request body on v3 — a bare JSON array — so there is no room in it for an OAuth credential: the portal reads every top-level entry as a command and requires method and query on each. On a server — anything that is not a browser or a worker — the SDK moves the token into an Authorization: Bearer header instead. A browser cannot follow: the portal answers the CORS preflight with Access-Control-Allow-Headers: origin, content-type, accept, so a request asking for that header would never leave at all.A browser therefore sends the credential in the query string?auth=<token>, appended to this one request shape. The portal reads it through the same dictionary as a body auth (CRestUtil::getRequestData() merges the query with the body), and a query parameter costs nothing on a preflight the request is already making for its Content-Type. Measured accepted on a live portal.This is the only place the SDK puts an OAuth token in a URL. It is not visible to anyone who could not already read it — in a frame app the token lives in the page's own JavaScript, and every non-batch call carries it in the request body — but it does reach the portal's web-server access log, and its diagnostic logger while an administrator has that switched on. If that matters for your deployment, run the batch on your own backend with B24OAuth or B24Hook, or use BatchV2.make() where the methods exist on restApi:v2.B24Hook appends nothing: its secret is already in the URL path.The day authorization appears in the portal's allow-list, a browser takes the same header path a server takes today.

Passing data between commands ($ref / $refArray)

A v3 batch can feed one command's output into a later command's params. Give the source command an as alias, then reference it with a $ref (single value) or $refArray (a field collected across the source's items[]) marker. The server performs the substitution — the SDK just passes the marker through. Use the BatchRefV3 helper to build the markers (it validates the path client-side):

import { BatchRefV3 as R } from '@bitrix24/b24jssdk'

const response = await $b24.actions.v3.batch.make({
  calls: [
    { method: 'tasks.task.list', as: 'tasks', params: { select: ['id'] } },
    {
      method: 'tasks.task.list',
      // the server expands this to `['id', 'in', [<ids collected from tasks.items>]]`
      params: { select: ['id', 'title'], filter: [['id', 'in', R.refArray('tasks.id')]] }
    }
  ]
})
  • R.ref('alias.path.to.field'){ $ref: '…' } — a single value. Only item (get) and items (list/tail) land in context; an add or update result does not, whatever shape that module gives it — a $ref over an add was refused with HTTP 400, INVALIDSELECTEXCEPTION on the on-premise build measured.
    What add and update return is the module's choice — three were measured and no two agree. The stock ORM traits give an id and a boolean ({ result: { id: 22 } } / { result: true }); tasks.task gives the whole object from add and a boolean nested one level deeper from update ({ result: { result: true } }); a module on a cloud sandbox gave the whole object from both. Full table on the call page.
  • R.refArray('alias.field'){ $refArray: '…' } — collects field across the alias's items[]. The path must contain a dot.
  • A bad path or an unresolved reference fails the batch with BITRIX_REST_V3_EXCEPTION_INVALIDSELECTEXCEPTION.
  • v3 only. These markers are not substituted in a v2 batch (actions.v2.batch.make) — there they become literal filter values and silently yield wrong/empty results.

Error Handling

The SDK no longer pre-validates batch methods against a client-side v3 list: every command is sent to the v3 batch endpoint and the server validates each one. A command that names a non-v3 method comes back as a server error for that command (and, per v3 batch semantics, an error aborts the batch). Use BatchV2.make() when the batch contains legacy methods, or split mixed batches by API version.

For successful requests, always check isSuccess and handle errors:

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

const response = await $b24.actions.v3.batch.make({
  calls: [
    { method: 'tasks.task.get', params: { id: 1 } },
    { method: 'tasks.task.get', params: { 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()

Examples

Getting Some Tasks

// @check-ignore: full example — processResult callback and BatchCommands tuple inference not in scope

BatchTasks.ts
import type { AjaxResult, Result } from '@bitrix24/b24jssdk'
import { AjaxError, B24Hook, LoggerFactory, SdkError } from '@bitrix24/b24jssdk'

type TaskItem = {
  id: number
  title: string
}

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

async function getMultipleItems(itemIds: number[], requestId: string): Promise<TaskItem[]> {
  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 calls = itemIds.map(id => [
    'tasks.task.get',
    {
      id,
      select: ['id', 'title']
    }
  ])

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

  if (!response.isSuccess) {
    throw new Error(`Problem: ${response.getErrorMessages().join('; ')}`)
  }

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

  return results
}

// Usage
const requestId = 'batch/tasks.task.get'
try {
  const itemIds = [1, 2, 3]
  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 })
  }
}

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): B24Frame cannot run a restApi:v3 batch — see Limitations. Use actions.v2.batch.make in the frame, or move the batch to your own backend (B24OAuth / B24Hook).