---
title: "Recipe: Export deals to CSV"
description: "Stream every CRM deal that matches a date filter into a CSV file using a webhook and FetchListV2."
canonical_url: "https://bitrix24.github.io/b24jssdk/docs/examples/dashboard-deals-csv"
last_updated: "2026-06-02"
---
# Recipe: Export deals to CSV

> Stream every CRM deal that matches a date filter into a CSV file using a webhook and FetchListV2.

> [!WARNING]
> We are still updating this page. Some data may be missing here — we will complete it shortly.

## 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"` in `package.json`)
- `@bitrix24/b24jssdk@^1.0.0`
- A Bitrix24 inbound webhook URL with the `crm` scope. Set it as `B24_HOOK` in your environment.

```bash
pnpm add @bitrix24/b24jssdk
export B24_HOOK="https://your-portal.bitrix24.com/rest/1/xxxxxxxxxxxxxxxx/"
```

## Full Example

`scripts/deals-to-csv.mjs`:

```js
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

```bash
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 configured `TypeB24` instance — no `await` initialization 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 an `AsyncGenerator<T[]>`. Each `for await` iteration yields up to `limit` records (default `50`) and uses cursor-based pagination on `idKey` ascending — the SDK takes care of stitching pages together.
- `Text.toB24Format(date)` converts a JS `Date` to the ISO format Bitrix24 expects in `crm.item.list` filters; passing a raw `Date` would not work.

## Limitations

- `crm.item.list` returns up to 50 records per page; this is a server-side limit, not an SDK setting.
- The generator does **not** support a custom `order` parameter — cursor pagination requires sorting by `idKey` ascending. Specifying `order` in `params` triggers a runtime warning and is stripped.
- `idKey` defaults to `'ID'` (uppercase) for v2 endpoints. CRM item methods return `id` (lowercase), so override it explicitly as in the example.
- One `B24Hook` instance 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()`](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/fetch-list-rest-api-ver2.md) — full method reference.
- [Node.js install + `B24Hook`](https://bitrix24.github.io/b24jssdk/raw/docs/getting-started/installation/nodejs.md) — webhook-based authentication for server scripts like this one.
- [Restrictions System](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/limiters.md) — choosing a `restrictionParams` preset.

## Sitemap

See the full [sitemap](/b24jssdk/sitemap.md) for all pages.
