| name | b24jssdk-rest |
| description | Call the Bitrix24 REST API through b24jssdk using the canonical actions.v{2,3}.*.make() surface. Covers call, batch, callList, fetchList, batchByChunk (and the v3-only native-keyset callTail/fetchTail) for both API versions, picking between v2 and v3, and the rules for the new AjaxResult shape. The legacy callMethod/callBatch/callListMethod/fetchListMethod surface is @deprecated for 3.0.0 — do not generate code against it. |
b24jssdk REST patterns (actions API)
Every example uses $b24 of type TypeB24, so the same code runs on B24Hook, B24Frame, and B24OAuth. The actions surface is published per API version under $b24.actions.v2.* and $b24.actions.v3.*.
The previous SDK surface — callMethod, callBatch, callBatchByChunk, callListMethod, fetchListMethod, plus AjaxResult.isMore() / getNext() / getTotal() — is @deprecated and scheduled for removal in 3.0.0 (see packages/jssdk/README-AI.md "Deprecation notice"). Do not generate new code against it.
Pick the API version
The SDK exposes both v2 and v3 under $b24.actions. The SDK no longer keeps a hardcoded v3 method allowlist — the server is the source of truth for which methods exist on a portal (the authoritative list is the portal's own OpenAPI document, rest.documentation.openapi). So $b24.actions.v3.* will send any method to the v3 endpoint; if the method isn't a v3 method, the server returns a METHODNOTFOUNDEXCEPTION (a soft error on the AjaxResult, not an SDK throw).
Method families that are known to exist on v3 today (non-exhaustive): tasks.task.* (incl. list), mail.*, humanresources.*, timeman.record.* (read-only), main.eventlog.* (incl. native tail), note.*, rest.application.*, rest.incomingwebhook.*, plus infrastructure (batch, scopes, rest.scope.list, rest.documentation.openapi).
Rule of thumb:
- Default to
$b24.actions.v2.* — it works for every classic method.
- Use
$b24.actions.v3.* when you specifically want the v3 representation of a method (camelCase fields, the unified {result} envelope, native tail/cursor, dotted relation select). Confirm a method exists on this portal's v3 via rest.documentation.openapi if unsure.
- Version auto-detection (the deprecated legacy
callMethod/callBatch shims) defaults to v2; v3 is opt-in only via the explicit actions.v3.* surface.
Decision tree
| Goal | Use |
|---|
| Single REST call | actions.v{2,3}.call.make |
| 2–50 related calls in one HTTP round-trip | actions.v{2,3}.batch.make |
| Many independent calls (>50) | actions.v{2,3}.batchByChunk.make |
| Read a small list (<1000 items) and process in memory | actions.v{2,3}.callList.make |
| Read a large list with low memory footprint | actions.v{2,3}.fetchList.make (async iterator) |
Read a v3 method that exposes a native tail (keyset) action — e.g. main.eventlog.tail | actions.v3.callTail.make / actions.v3.fetchTail.make (v3 only) |
Aggregate (sum/avg/min/max/count/countDistinct) on a v3 method that exposes an *.aggregate action | actions.v3.aggregate.make (v3 only, @experimental — unverified live; fall back to callList + reduce if the endpoint isn't available) |
There is no manual-pagination path any more — the list helpers page for you. Two mechanisms exist, and they are not interchangeable:
callList / fetchList emulate a cursor on top of the list action by injecting a [idField, '>', n] condition into filter and forcing order. Works for any *.list method (v2 and v3).
callTail / fetchTail (v3 only) drive the server's native tail action via its cursor: { field, value, order, limit } parameter. Use these when a method publishes a *.tail endpoint. The cursor field must not appear in filter (the server rejects it), is auto-added to select, and order: 'DESC' requires an explicit initialValue.
call.make — single call
import { EnumCrmEntityTypeId } from '@bitrix24/b24jssdk'
interface CrmItem { id: number; title: string; stageId: string }
const response = await $b24.actions.v2.call.make<{ item: CrmItem }>({
method: 'crm.item.get',
params: {
entityTypeId: EnumCrmEntityTypeId.deal,
id: 42
},
requestId: 'deal-42'
})
if (!response.isSuccess) {
throw new Error(response.getErrorMessages().join('; '))
}
const deal = response.getData()!.result.item
For a v3 method:
interface TaskItem { id: number; title: string }
const response = await $b24.actions.v3.call.make<{ task: TaskItem }>({
method: 'tasks.task.get',
params: { id: 1, select: ['id', 'title'] },
requestId: 'task-1'
})
const task = response.getData()!.result.task
batch.make — array form
import type { AjaxResult } from '@bitrix24/b24jssdk'
interface Contact { id: number; name: string }
const response = await $b24.actions.v2.batch.make<{ item: Contact }>({
calls: [
['crm.item.get', { entityTypeId: EnumCrmEntityTypeId.contact, id: 1 }],
['crm.item.get', { entityTypeId: EnumCrmEntityTypeId.contact, id: 2 }]
],
options: {
isHaltOnError: true,
returnAjaxResult: true,
requestId: 'batch-1'
}
})
if (!response.isSuccess) throw new Error(response.getErrorMessages().join('; '))
const results = response.getData()! as AjaxResult<{ item: Contact }>[]
for (const r of results) {
if (r.isSuccess) console.log(r.getData()!.result.item)
}
batch.make — named object form
interface Contact { id: number; name: string }
interface Deal { id: number; title: string }
const response = await $b24.actions.v2.batch.make<{ item: Contact } | { item: Deal }>({
calls: {
Contact: { method: 'crm.item.get', params: { entityTypeId: 3, id: 1 } },
Deal: ['crm.item.get', { entityTypeId: 2, id: 2 }]
},
options: { isHaltOnError: true, returnAjaxResult: true, requestId: 'batch-named' }
})
const data = response.getData()! as Record<string, AjaxResult<{ item: Contact } | { item: Deal }>>
console.log(data.Contact.getData()!.result.item)
console.log(data.Deal.getData()!.result.item)
batch.make — partial errors (v2 only)
Set isHaltOnError: false to collect per-command failures. v3 batch is all-or-nothing — partial errors are not surfaced. If any command in a v3 batch fails, the whole batch fails (see README-AI.md "Limitations").
To feed one v3 command's output into a later one, give it an as alias and reference it with the BatchRefV3 markers (import { BatchRefV3 } from '@bitrix24/b24jssdk'): BatchRefV3.ref('alias.item.id') (single value) or BatchRefV3.refArray('alias.id') (a field collected across the alias's items[]). The server does the substitution.
const response = await $b24.actions.v2.batch.make<{ item: Contact }>({
calls: arrayOfCalls,
options: { isHaltOnError: false, returnAjaxResult: true }
})
const items = response.getData()! as AjaxResult<{ item: Contact }>[]
const successes = items.filter((r) => r.isSuccess)
const failures = items.filter((r) => !r.isSuccess).map((r) => r.getErrorMessages().join('; '))
For object / named-command calls (calls: { name: { method, params } }), the outer Result keys each failure by the command name — use response.getErrorsByKey() / getErrorMessagesByKey() to get a { name: error } map instead of iterating per-item results.
batchByChunk.make — large batches
Chunk size is 50 per Bitrix24 batch limit. The action splits and re-aggregates:
import type { BatchCommandsArrayUniversal } from '@bitrix24/b24jssdk'
const calls: BatchCommandsArrayUniversal = ids.map((id) =>
['crm.item.get', { entityTypeId: EnumCrmEntityTypeId.deal, id }] as const
)
const response = await $b24.actions.v2.batchByChunk.make<{ item: CrmItem }>({
calls,
options: { isHaltOnError: false, requestId: 'bulk-1' }
})
if (!response.isSuccess) throw new Error(response.getErrorMessages().join('; '))
const data = response.getData()!
const items = data.map((row) => row.item)
callList.make — small lists in memory
Loads up to 1000 items into a single array. Internally pages with a keyset cursor on cursorIdKey (which defaults to idKey).
import { Text } from '@bitrix24/b24jssdk'
interface CrmItem { id: number; title: string }
const sixMonthsAgo = new Date()
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6)
const response = await $b24.actions.v2.callList.make<CrmItem>({
method: 'crm.item.list',
params: {
entityTypeId: EnumCrmEntityTypeId.company,
filter: {
'=%title': 'A%',
'>=createdTime': Text.toB24Format(sixMonthsAgo)
},
select: ['id', 'title']
},
idKey: 'id',
customKeyForResult: 'items',
requestId: 'companies-1'
})
if (!response.isSuccess) throw new Error(response.getErrorMessages().join('; '))
const items = response.getData()!
order is ignored by callList.make. The action forces order: { [cursorIdKey]: 'ASC' } for cursor stability and logs a warning when you pass an order (see the order warning in actions/v2/call-list.ts). Use filter to narrow results.
fetchList.make — large lists, streaming
Async iterator that yields chunks. Same shape as callList.make plus an optional limit for v3.
const generator = $b24.actions.v2.fetchList.make<CrmItem>({
method: 'crm.item.list',
params: {
entityTypeId: EnumCrmEntityTypeId.deal,
filter: { '!stageId': ['WON', 'LOSE'] },
select: ['id', 'title', 'stageId']
},
idKey: 'id',
customKeyForResult: 'items'
})
for await (const chunk of generator) {
for (const deal of chunk) await processDeal(deal)
}
For v3:
const generator = $b24.actions.v3.fetchList.make<EventLogItem>({
method: 'main.eventlog.list',
params: {
filter: [['timestampX', '>=', Text.toB24Format(sixMonthsAgo)]],
select: ['id', 'userId']
},
idKey: 'id',
customKeyForResult: 'items',
limit: 100
})
Note the v2 vs v3 filter difference. v2 uses prefix-keyed objects; v3 uses arrays of [field, op, value] triples — for nested groups build them with the typed FilterV3 helper (import { FilterV3 } from '@bitrix24/b24jssdk'). See the b24jssdk-filtering skill.
idKey, cursorIdKey and customKeyForResult cheat sheet
idKey is the id field in the response (the cursor reads its value); cursorIdKey is the field in the request used for order and the > page filter, and it defaults to idKey. They differ only when a method sorts/filters by one name but returns another — most notably tasks.task.list on v2 (request ID, response id). On the v3 endpoint tasks.task.list is all-lowercase (id for both request and response, rows under result.items), so no cursorIdKey override is needed.
| Method | idKey (response) | cursorIdKey (request) | customKeyForResult |
|---|
crm.item.list (v2) | 'id' | — (= idKey) | 'items' |
crm.deal.list, crm.contact.list, … (classic v2) | 'ID' (default) | — (= idKey) | omit (default result) |
tasks.task.list (v2) | 'id' | 'ID' | 'tasks' |
tasks.task.list (v3) | 'id' | — (= idKey) | 'items' |
disk.folder.getchildren | 'ID' (default) | — (= idKey) | omit |
main.eventlog.list (v3) | 'id' | — (= idKey) | 'items' |
Wrong customKeyForResult makes getData() return an empty array — there is no error. If you're getting [] and expect data, this is the first thing to check.
AjaxResult — new shape
const res = await $b24.actions.v2.call.make<{ item: CrmItem }>({
method: 'crm.item.get',
params: { entityTypeId: 2, id: 10 }
})
res.isSuccess
res.getData()
res.getErrorMessages()
res.getErrors()
res.getErrorsByKey()
res.getStatus()
res.getQuery()
getData() returns undefined when the call did not succeed — the new typing forces you to either check isSuccess first, or assert with !. Both patterns appear in the canonical SDK tests (test/integration/js-docs/actions-v{2,3}.spec.ts).
Removed from the public surface for 3.0.0:
isMore(), hasMore() — was tied to v2 envelope next
getTotal() — was tied to v2 envelope total. v3 has no total in the envelope; for a count use actions.v3.aggregate.make with select: { count: ['id'] } on a method that exposes an *.aggregate action (most don't yet — otherwise reduce a callList client-side).
getNext(), fetchNext() — replaced by callList.make / fetchList.make
Null result is passthrough
A per-command result inside a batch can legitimately be null (e.g. im.chat.get with non-matching params — see issue #23). Type the generic as T | null and handle the null branch — the SDK no longer coerces to {}.
const response = await $b24.actions.v2.batch.make<{ result: ChatInfo | null }>({
calls: { Chat: ['im.chat.get', { chat_id: 999999 }] },
options: { returnAjaxResult: true }
})
const chat = (response.getData() as Record<string, AjaxResult<{ result: ChatInfo | null }>>)
.Chat.getData()!.result.result
if (chat === null) {
}
Error handling — quick template
import { AjaxError, SdkError } from '@bitrix24/b24jssdk'
try {
const res = await $b24.actions.v2.call.make({
method: 'crm.item.get',
params: { entityTypeId: 2, id: 999_999 }
})
if (!res.isSuccess) {
logger.warn('non-success', res.getErrorMessages())
return
}
return res.getData()!.result.item
} catch (e) {
if (e instanceof AjaxError) {
logger.error('REST error', { code: e.code, status: e.status, message: e.message, requestInfo: e.requestInfo })
} else if (e instanceof SdkError) {
logger.error('SDK error', { code: e.code, message: e.message })
} else {
throw e
}
}
For tuning retry/throw behaviour per error code see the hardErrorCodes / softErrorCodes / retryOnNetworkError section in the b24jssdk-core skill.
Discover available v3 methods (OpenAPI)
The portal is the source of truth for which v3 methods exist — the SDK keeps no allowlist. Ask the portal with rest.documentation.openapi, an ordinary v3 call that returns the portal's own OpenAPI 3.0.0 document (every v3 method available on that portal, with each method's request fields and response shape). Useful for an agent before generating a call: enumerate real method names and field names instead of guessing.
type OpenApiDoc = {
openapi: string
tags: Array<{ name: string, description: string }>
paths: Record<string, { post?: { tags?: string[], requestBody?: unknown } }>
}
const res = await $b24.actions.v3.call.make<OpenApiDoc>({ method: 'rest.documentation.openapi' })
if (!res.isSuccess) {
throw new Error(res.getErrorMessages().join('; '))
}
const doc = res.getData()?.result
const methods = Object.keys(doc?.paths ?? {}).map(p => p.replace(/^\//, ''))
const hasNotes = Boolean(doc?.paths?.['/note.collection.list'])
- The document is portal-specific (reflects installed modules + token scopes) and large (100 KB+). Fetch it once and cache it — it's response-only and stable within a session; don't re-request per interaction.
- Each
paths['/method.name'].post.requestBody schema lists the accepted parameters (often with an example of real field names) — read those instead of inventing select fields.
- Full guide: Discovering v3 methods.
Anti-patterns
- ❌
$b24.callMethod(...), $b24.callBatch(...), etc. — @deprecated, removed in 3.0.0. Use the actions API.
- ❌
res.getTotal() / res.isMore() / res.getNext() — @deprecated, throw on v3. Use callList / fetchList for paging; for a count use actions.v3.aggregate.make (count/countDistinct) on a method that exposes an *.aggregate action.
- ❌ Calling
$b24.actions.v3.call.make({ method: 'crm.item.get', ... }) — crm.* is v2-only, so the v3 server returns a METHODNOTFOUNDEXCEPTION soft error (response.isSuccess === false); use actions.v2.* for CRM. (The SDK no longer pre-flight-throws here.)
- ❌ Passing
order to callList.make — silently ignored with a warning. Narrow with filter instead.
- ❌
customKeyForResult: 'result' for crm.item.list — wrong, use 'items'. Otherwise you'll get an empty list silently.
- ❌
idKey: 'ID' for crm.item.list — wrong, use 'id'. The classic crm.deal.list is the opposite.
- ❌
idKey: 'ID' alone for tasks.task.list — the response id is lowercase id, so the cursor can't read it and paging silently stops after 50. Use idKey: 'id', cursorIdKey: 'ID'.
- ❌
Promise.all over callList.make for parallel paging — internal cursor pagination is sequential by design; you'll get duplicates or skipped rows.
- ❌ Hand-paging a v3 list method by the
nextCursor it returns (e.g. note.*) — callList / fetchList page via their own idKey cursor and walk every page; nextCursor is informational and the SDK ignores it. Just use the list helpers with idKey + customKeyForResult.
- ❌
B24Hook in a browser bundle — leaks the webhook secret. Use B24Frame there.
Cross-reference
For v3 filter dialect / ordering / NULL handling, use the b24jssdk-filtering skill.