Goal
Pull every CRM deal closed in a date window from a Bitrix24 portal and write it into a CSV file. Memory stays flat regardless of how many deals match — FetchListV2.make() streams pages of 50 records at a time.
Stack & Prerequisites
- Node.js 20+, ESM (
"type": "module"inpackage.json) @bitrix24/b24jssdk@^1.0.0- A Bitrix24 inbound webhook URL with the
crmscope. Set it asB24_HOOKin your environment.
pnpm add @bitrix24/b24jssdk
export B24_HOOK="https://your-portal.bitrix24.com/rest/1/xxxxxxxxxxxxxxxx/"
Full Example
scripts/deals-to-csv.mjs:
import { writeFile } from 'node:fs/promises'
import {
B24Hook,
EnumCrmEntityTypeId,
ParamsFactory,
Text
} from '@bitrix24/b24jssdk'
const hookUrl = process.env.B24_HOOK
if (!hookUrl) {
console.error('B24_HOOK is not set')
process.exit(1)
}
// Closed in the previous calendar month.
const now = new Date()
const from = new Date(now.getFullYear(), now.getMonth() - 1, 1)
const to = new Date(now.getFullYear(), now.getMonth(), 1)
const b24 = B24Hook.fromWebhookUrl(hookUrl, {
// Lower per-second pressure than `getDefault()` — good for reporting jobs
// that run alongside live user traffic on the same portal.
restrictionParams: ParamsFactory.getBatchProcessing()
})
const generator = b24.actions.v2.fetchList.make({
method: 'crm.item.list',
params: {
entityTypeId: EnumCrmEntityTypeId.deal,
filter: {
'>=closedAt': Text.toB24Format(from),
'<closedAt': Text.toB24Format(to),
stageSemanticId: 'S' // closed-won
},
select: ['id', 'title', 'opportunity', 'currencyId', 'closedAt', 'assignedById']
},
idKey: 'id',
customKeyForResult: 'items',
requestId: 'deals-to-csv'
})
const rows = ['id;title;amount;currency;closedAt;assignedById']
let count = 0
for await (const chunk of generator) {
for (const deal of chunk) {
rows.push([
deal.id,
JSON.stringify(deal.title ?? ''),
deal.opportunity ?? '',
deal.currencyId ?? '',
deal.closedAt ?? '',
deal.assignedById ?? ''
].join(';'))
}
count += chunk.length
process.stderr.write(`fetched: ${count}\r`)
}
const file = `deals-${from.toISOString().slice(0, 7)}.csv`
await writeFile(file, rows.join('\n'), 'utf8')
console.log(`\nwrote ${count} rows to ${file}`)
Run It
node scripts/deals-to-csv.mjs
# fetched: 4350
# wrote 4350 rows to deals-2026-04.csv
How It Works
B24Hook.fromWebhookUrl()parses the webhook URL and gives you a configuredTypeB24instance — noawaitinitialization needed.ParamsFactory.getBatchProcessing()swaps the default rate-limit profile for one tuned for long-running scans, so the script doesn't fight live UI requests for the portal queue.actions.v2.fetchList.make()returns anAsyncGenerator<T[]>. Eachfor awaititeration yields up tolimitrecords (default50) and uses cursor-based pagination oncursorIdKey(which defaults toidKey) ascending — the SDK takes care of stitching pages together.Text.toB24Format(date)converts a JSDateto the ISO format Bitrix24 expects incrm.item.listfilters; passing a rawDatewould not work.
Limitations
crm.item.listreturns up to 50 records per page; this is a server-side limit, not an SDK setting.- The generator does not support a custom
orderparameter — cursor pagination requires sorting bycursorIdKey(defaultidKey) ascending. Specifyingorderinparamstriggers a runtime warning and is stripped. - The walk stops after 10 000 pages — 500 000 deals at 50 a page — and throws
JSSDK_ACTION_MAX_PAGES_EXCEEDED. That is a backstop against a runaway, not a cap on this export; pass a highermaxPagesif a portal genuinely holds more. BecausefetchListstreams, every page written to the CSV before that point is already on disk. idKeydefaults to'ID'(uppercase) for v2 endpoints. CRM item methods returnid(lowercase), so override it explicitly as in the example. If a method sorts/filters by a different field name than it returns (e.g.tasks.task.listsorts byIDbut returnsid), also setcursorIdKey—crm.item.listaccepts the lowercase name in both places, which is why this script needs onlyidKey. Get that pair wrong and the walk throwsJSSDK_ACTION_CURSOR_STALLEDrather than looping: see Errors.- One
B24Hookinstance corresponds to a single user account on the portal. If the deals you need are owned by another user, the webhook must be created by that user (or by an account with cross-user access). - For very large windows that exhaust the per-day method-call budget, split the date range into smaller chunks and run them sequentially.
See also
FetchListV2.make()— full method reference.- Node.js install +
B24Hook— webhook-based authentication for server scripts like this one. - Restrictions System — choosing a
restrictionParamspreset.
CRM analytics
Stream all deals via actions.v2.fetchList.make, group by stage, print a funnel report (counts, conversion %, avg ticket, win rate).
Error handling
Demonstrates the four error layers (SdkError / AjaxError / network / soft) and the hardErrorCodes / softErrorCodes / retryOnNetworkError knobs on the restriction manager.