Overview
Use BatchV3.make() to execute up to 50 REST API commands in a single request, minimizing network round-trips.
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:
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],
// ...
}
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)
options. 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:
T is one command's payload in every row — not the collection.
[{ 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.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.
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. timeon eachAjaxResultis 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.
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. Onlyitem(get) anditems(list/tail) land in context; anaddorupdateresult does not, whatever shape that module gives it — a$refover anaddwas refused with HTTP 400,INVALIDSELECTEXCEPTIONon the on-premise build measured.Whataddandupdatereturn 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.taskgives the whole object fromaddand a boolean nested one level deeper fromupdate({ 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: '…' }— collectsfieldacross the alias'sitems[]. 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
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
Callfor single calls. - For working with lists: Use
CallListfor retrieving large volumes of data. - For step-by-step processing: Use
FetchListfor 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):
B24Framecannot run arestApi:v3batch — see Limitations. Useactions.v2.batch.makein the frame, or move the batch to your own backend (B24OAuth/B24Hook).