This guide lists everything that was @deprecated through the 2.x line and is gone as of 3.0.0, with the canonical replacement for each symbol.
2.x they kept working and emitted a
runtime deprecation warning on every call; in 3.0.0 they are deleted. Code that
still calls them fails to compile — which is the good case. Plain JavaScript gets
no such warning and fails at run time with is not a function, so run the
search below before you upgrade, not after.diff examples for every method are in the v0 → v1 guide under "Deprecated" — this page is the at-a-glance removal checklist.What was removed in 3.0.0
Legacy REST shortcuts on AbstractB24
These instance shortcuts ignore the restApi:v2 / restApi:v3 split and are replaced by the explicit action surface:
- await b24.callMethod('crm.deal.list', { filter: { '>id': 123 } }, 100)
+ await b24.actions.v2.call.make({
+ method: 'crm.deal.list',
+ params: { filter: { '>id': 123 }, start: 100 },
+ requestId: 'unique-request-id'
+ })
callListMethod is the one that is not a drop-in
Four of the five removals were thin shims — they normalised an argument and
called the replacement, so swapping them is mechanical. callListMethod was not:
it carried its own paging loop, and the replacement pages differently. Three
things change.
1. There is no progress callback. callListMethod's third argument
reported percent-complete; callList.make has no equivalent, because it cannot
know the total in advance. Report progress yourself from
fetchList.make,
which hands you a page at a time — and if you want a percentage rather than a
running count, take the denominator from one call.make first, since
getTotal() is still the
only way to count rows under restApi:v2:
// @check-ignore: partial snippet — $b24 declared elsewhere
const first = await $b24.actions.v2.call.make({
method: 'crm.deal.list',
params: { filter: { '>id': 0 } }
})
const total = first.getTotal() // v2 only; 0 under restApi:v3
const rows: unknown[] = []
for await (const chunk of $b24.actions.v2.fetchList.make({ method: 'crm.deal.list' })) {
rows.push(...chunk)
const percent = total > 0 ? Math.round((100 * rows.length) / total) : 100
console.log(`${percent}%`)
}
2. A caller-supplied order is ignored. callListMethod walked by start
offset, so your order survived. callList / fetchList walk by an >id
cursor, which requires ordering by that cursor — so order is stripped from
the type and, if you reach past the type from JavaScript, dropped at run time
with a warning in the log. Narrow with filter instead; if you need rows in a
particular order, sort them after collecting, or page by hand with call.make
and getNext().
3. The walk is cursor-based, so the id field matters. Offset paging did not
care what the id was called; cursor paging does. Pass idKey as the response
spells it and cursorIdKey as the request wants it — for tasks.task.list
that is idKey: 'id', cursorIdKey: 'ID'. Get this wrong and the page condition
matches nothing, which the SDK now catches: the walk stops with
JSSDK_ACTION_CURSOR_STALLED rather than looping for ever.
- const result = await b24.callListMethod(
- 'crm.deal.list',
- { filter: { '>id': 0 }, order: { DATE_CREATE: 'DESC' } },
- (percent) => console.log(`${percent}%`),
- 'items'
- )
+ // `order` has no equivalent — sort after collecting, and report progress
+ // from `fetchList` as shown above.
+ const result = await b24.actions.v2.callList.make({
+ method: 'crm.deal.list',
+ params: { filter: { '>id': 0 } },
+ idKey: 'ID',
+ customKeyForResult: 'items'
+ })
AbstractB24.batchSize
Maximum length for batch response.
The static const is removed. Inline the value 50 if you relied on it.
- if (size < AbstractB24.batchSize) {
+ if (size < 50) {
// some code ...
}
AjaxResult paging helpers are not removed after all
All five read the restApi:v2 envelope fields next / total. An earlier plan had them removed in 3.0.0 alongside the rest of the legacy surface; that plan is withdrawn. All five stay:
They were batched for removal on the criterion "reads a restApi:v2 envelope field". That criterion describes the protocol, not the user: restApi:v2 is not going anywhere for a long time, most of the Bitrix24 REST surface still lives there, and deleting a method that works today would break running code with no benefit on offer. getTotal() is the sharpest case — it is the only way to count rows under restApi:v2 (the list helpers iterate without exposing total, SuccessPayload omits it by design, and the actions.v3.aggregate.make action is restApi:v3-only and @experimental — its contract is measured now, but no shipped module publishes *.aggregate on any portal yet measured, so there is nothing to count with it yet). Removing it would have been a capability regression dressed as a cleanup.
Nothing here needs migrating. If you already moved to the list helpers, stay there — they are still the better tool, and the only one that works under restApi:v3. If you did not, you do not have to.
This is a decision with a trigger rather than an open-ended promise, and the trigger differs by method:
getTotal()/isMore()/hasMore()— revisited when a shipped module publishes an*.aggregateaction andactions.v3.aggregateloses@experimental(a v3 count would then exist in practice, givinggetTotal()a replacement for the first time), or when arestApi:v2sunset date is announced.getNext()/fetchNext()— arestApi:v2sunset date, and nothing else. They already have a working replacement, soaggregatematuring changes nothing for them.
restApi:v2 only, and always were. Under restApi:v3:getTotal()returns0andisMore()returnsfalse— because the field is absent, not because the count is zero or the rows ran out. Do not branch on either.getNext()/fetchNext()throwSdkError({ code: 'JSSDK_CORE_METHOD_NOT_SUPPORT_IN_API_V3' }). Deliberately: returningfalsewould be indistinguishable from "last page".
For new code the list helpers are still the recommendation — callList.make (collect everything) or fetchList.make (async generator, one page per iteration). They hide the offset bookkeeping and work under both protocol versions:
- // manual paging: yours to maintain, and v2-only
- const res = await b24.actions.v2.call.make({ method: 'crm.deal.list' })
- let list = res.getData()!.result
- while (res.isMore()) { res = await res.getNext(http); list.push(...res.getData()!.result) }
+ // the list helper iterates for you — `chunk` is the next array of items
+ for await (const chunk of b24.actions.v2.fetchList.make({ method: 'crm.deal.list' })) {
+ // handle chunk
+ }
LoggerBrowser and LoggerType
Replaced by the universal Logger / LoggerFactory system. The LoggerType enum is removed with no replacement — LoggerFactory manages levels itself, so just drop the import:
- import { LoggerBrowser, LoggerType } from '@bitrix24/b24jssdk'
+ import { LoggerFactory } from '@bitrix24/b24jssdk'
- const $logger = LoggerBrowser.build('MyApp', import.meta.env?.DEV === true)
+ const $logger = LoggerFactory.createForBrowser('MyApp', import.meta.env?.DEV === true)
- $logger.info('response', dataList)
+ $logger.info('response', { someInfo: dataList })
Result<T> defaults to unknown, not any
What changed. Result<T> and IResult<T>
declared T = any. They now declare
T = unknown. The same narrowing reaches
TypeCallParams: its catch-all index signature is
[key: string]: unknown rather than any, the nested
params field is Record<string, unknown>, and filter is
TypeFilterV2 | TypeFilterV3. The per-version
TypeCallParamsV2 / TypeCallParamsV3 still
narrow filter to their own dialect, so nothing changes if you were already
using those.
Who is affected. Anyone who wrote Result without a type
argument and then read a field off it. That compiled silently before, because
any propagates:
- const batch: Result = await b24.actions.v2.batch.make({ calls, options })
- const list = batch.getData().CompanyList.items // any — no check, no help
+ const batch = await b24.actions.v2.batch.make<{ items: Company[] }>({ calls, options })
+ const data = batch.getData() as { CompanyList?: { items?: Company[] } } | undefined
+ const list = data?.CompanyList?.items ?? []
What to do. Name the payload. Every action takes a generic —
call.make<T>, batch.make<T>, callList.make<T> — and that is the
better fix, because the type then flows through the whole read. Where the shape
genuinely is not known until run time, a single narrowing cast at the point of
reading keeps the rest of the code checked.
[key: string] outright was considered and rejected on a count of our own
examples: crm.item.getis{ entityTypeId, id }, and those keys are the
payload rather than an escape hatch — a closed type would stop most of the
documentation from compiling. unknown removes any from the type without
refusing the ordinary way the API is called.batch result no longer needs narrowing by hand. It really does come back
in one of four shapes, decided by whether the commands went in as a named record
or an array and by returnAjaxResult — but batch.make is overloaded on exactly
those arguments now (#518), so the compiler picks the right one for you. Casts
written against the old union can go. See
the return-value table.Runtime: Node 20 was dropped in 3.0.0
3.0.0 raises the Node floor. engines.node moves from ^20.0.0 || >=22.0.0 to
>=22.0.0, so Node 20 is no longer supported.
Node 20 reached end-of-life on 2026-04-30 — it no longer receives security fixes
— and CI has tested only Node 22 and 24 for some time, so 2.x advertised a
version nothing exercised. 3.0.0 closes that gap.
What this means for you:
- On Node 22 or newer, nothing changes.
- On Node 20,
npm installprints anEBADENGINEwarning, and underengine-strict=true(pnpm's default inside a workspace) the install fails outright. Move to Node 22 (LTS) or newer before upgrading.
It is a packaging floor, not an API change: no code has to change, only the Node version you run on. There is nothing to migrate in your source.
Behaviour: v3 errors are classified by response category
This is the one change in 3.0.0 that alters what working code does without
producing a compile error, so it is worth reading even if your build is clean.
On restApi:v3, an error that arrived in the v3 error envelope carrying an HTTP
4xx other than 401, 408 or 429 is now returned inside AjaxResult — soft —
whatever its code. Through the 2.x line that was opt-in behind a
classifyV3ErrorsByCategory restriction parameter, which defaulted to false;
in 3.0.0 the rule is simply the behaviour and the parameter is gone.
If you were passing the parameter
Delete the line. TypeScript reports it as an unknown property, and the behaviour you were asking for is what you now get by default.
await $b24.setRestrictionManagerParams({
...ParamsFactory.getDefault(),
- classifyV3ErrorsByCategory: true,
hardErrorCodes: ['MY_APP_BAD_PAYLOAD']
})
If you were not
Then the delivery of some v3 errors changes under you, and nothing fails to
compile. A v3 4xx that used to reject the promise now resolves it, so a
try / catch around the call stops firing and control falls through into the
success path — where response.getData() is undefined and the next line reads
a field off it.
// Before 3.0.0 this caught an unlisted v3 4xx. It no longer does.
try {
const response = await $b24.actions.v3.call.make({ method: 'main.eventlog.list', params: {} })
console.log(response.getData()!.result)
}
catch (error) {
console.error(error)
}
// The shape that works on both: check the result, then catch what is left.
const response = await $b24.actions.v3.call.make({ method: 'main.eventlog.list', params: {} })
if (!response.isSuccess) {
console.error(response.getErrorMessages().join('; '))
}
else {
console.log(response.getData()!.result)
}
Reading isSuccess was already required for every code pinned soft — the nine
built-in v3 codes and anything in your own softErrorCodes — so a caller who had
that check was already handling this path for some errors and now handles it for
more. Nothing that was soft in 2.x becomes thrown. The change is in one
direction only.
If you need the old behaviour for a specific code
Pin it. hardErrorCodes is consulted before the category rule, so a code listed
there still throws:
import { ParamsFactory } from '@bitrix24/b24jssdk'
await $b24.setRestrictionManagerParams({
...ParamsFactory.getDefault(),
hardErrorCodes: ['BITRIX_REST_V3_EXCEPTION_INVALIDPAGINATIONEXCEPTION']
})
There is no switch that restores the old classification wholesale, and that is deliberate: a dual-behaviour flag with no removal date is how a compatibility surface becomes permanent, which is the reason this one had a scheduled end (#480).
What is untouched
5xx, 401, 408 and 429, the whole of restApi:v2 — whose flat error body is not a
v3 envelope — and every code pinned in the built-in hard list, including
…INSUFFICIENTSCOPEEXCEPTION at 403. Full detail in
Error Codes.
Behaviour: a browser now uses the fetch adapter
Another change that alters what working code does without producing a compile error.
Axios picks its adapter by walking ['xhr', 'http', 'fetch'] and taking the
first supported entry, so anywhere XMLHttpRequest exists — a window, a worker —
it picked XHR, by list order rather than by merit. In 3.0.0 the SDK asks for
fetch in a browser-like runtime. Node is untouched: XHR does not exist there,
the http adapter is already what gets chosen, and it stays chosen.
"Browser-like" means a browser or a worker — the same set as
isBrowserLikeRuntime()
below, which is the predicate the SDK asks.
Three things follow, and only in a browser.
1. A blocked redirect now errors. maxRedirects: 0 is read by fetch (as
redirect: 'manual') and was ignored outright by XHR. The SDK sets it on one
request — a restApi:v3 batch on a non-hook transport, which carries an access
token that a redirect to a subdomain would take along — so on that one path a
redirecting deployment now fails with JSSDK_HTTP_REDIRECT_BLOCKED where it used
to complete the hop silently. Ordinary calls are unaffected — unless you set
maxRedirects: 0 yourself in httpOptions, which puts every status-0 answer in
the same bucket, since an opaque response cannot be told from a dropped
connection.
2. A test double that stubs XMLHttpRequest stops intercepting. jsdom
counts as a browser here, so a suite mocking XHR no longer sees SDK traffic.
3. onUploadProgress on your own requests through ajaxClient degrades
outside Chromium: axios gates it on request streaming, which Firefox and Safari
do not offer, where XHR always delivered progress events.
The opt-out
Every entry point takes httpOptions at construction, merged over the SDK's own
axios defaults, so naming an adapter wins:
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(
'https://your-portal.bitrix24.com/rest/1/SECRET',
{ httpOptions: { adapter: 'xhr' } }
)
B24Hook, B24OAuth and B24Frame take the same option — and for a frame app, which is what this change is about, pass it to initializeB24Frame(), the only supported way to build a B24Frame: initializeB24Frame({ httpOptions: { adapter: 'xhr' } }). It is a
narrow slice of AxiosRequestConfig — TypeHttpOptions — deliberately: the keys
the SDK needs in order to reach the portal are not offered there (a key outside
the list is dropped at construction and its name logged), and they stay
reachable through
ajaxClient.defaults.
Behaviour: a cycling or backwards cursor now stops the walk
2.3.0 taught every list and tail walker to stop when the pagination cursor
repeats (JSSDK_ACTION_CURSOR_STALLED). That check compares against the
immediately preceding value, and 3.0.0 closes the two gaps it left.
A server that alternates between two pages — cursor A, B, A, B, … —
never repeats the value just sent, so it walked for ever: the streaming helpers
yielded the same rows in a loop, the eager ones grew an array until the process
died. Every walk here asks for rows strictly past the cursor ([cursorIdKey, '>', cursor]
for a list walk, cursor: { field, value, order } for a tail one), so the cursor
must advance in the walk's own direction on every page. A value that goes the
other way is now SdkError with code JSSDK_ACTION_CURSOR_WENT_BACKWARDS. Since
every cycle has to step backwards somewhere, this catches a cycle of any
length — and a cursor that simply runs backwards, which loses rows rather than
looping and which nothing looked for before.
A stalled page that was shorter than an earlier one ended the walk as end-of-data, whatever the cursor did — so the result came back truncated, with rows repeated from an earlier page, reported as success. The cursor is now checked before the end-of-data stop: a row at or before the cursor cannot be in an answer that honoured a strictly-greater condition, however short the page, so a short page whose cursor did not move is a server ignoring the condition rather than the data running out.
What this means for your code
Nothing, if your walks terminate — a correct walk advances its cursor on every
page, which is what the check tests. Measured against a live portal: a v3
callList over main.eventlog.list returns 68 rows, all unique and strictly
ascending, and a callTail with order: 'DESC' returns the same rows strictly
descending. Both pass.
If you catch the stall code, catch its companion beside it:
import { SdkError } from '@bitrix24/b24jssdk'
try {
const response = await $b24.actions.v3.callList.make({
method: 'tasks.task.list',
customKeyForResult: 'items'
})
console.log(response.getData())
}
catch (error) {
if (
error instanceof SdkError
&& (error.code === 'JSSDK_ACTION_CURSOR_STALLED' || error.code === 'JSSDK_ACTION_CURSOR_WENT_BACKWARDS')
) {
// Pagination is broken for this method — check `idKey` / `cursorIdKey`, or
// `cursorField` for a tail walk. Nothing collected is worth keeping.
}
else {
throw error
}
}
100 then '100'), ids of
different lengths ('9' then '10', where
lexicographic order disagrees with numeric), mixed-case values that a
case-insensitive portal collation may order differently from JavaScript, and
timestamps that a daylight-saving transition reorders — one whose UTC offset
changes across it, and one that states no offset at all (a bare
2024-10-27 02:15:00, where the repeated wall-clock hour makes
the later instant sort first). The guard declines to judge rather than
guessing: a false "backwards" would reject a walk that is working, which is
worse than missing one that is not.Behaviour: a worker is no longer read as a server
getEnvironment() gained a fourth member, Environment.WORKER, and Web, Shared and
Service Workers now report it instead of falling through to UNKNOWN.
A runtime that reports a Node version stays NODE even when it models itself on the
Worker API — a Deno worker and a Cloudflare Worker with nodejs_compat both do —
because those are servers, and a webhook belongs there. A Bun worker reports
NODE for the same reason, and a Cloudflare Worker without nodejs_compat is
UNKNOWN; the
Environment page carries the
detail.
The enum member is the visible half. The half that changes behaviour is that the SDK no longer treats a worker as a server:
- it no longer sets a
User-Agentdefault header there — a forbidden header in any browser context, which the browser was dropping anyway; - the "a webhook is for the server only" warning now fires in a worker, where it did not. Code shipped to a worker is exactly as public as code on the main thread, and the secret is in the bundle either way;
TelegramHandlerno longer sends from a worker. It warns in the console as it does in a browser, andtestConnection()returnsfalserather than putting the bot token in a URL — previously a worker reached the silent fallback, and the connection test made the request.
This is the same runtime set as the adapter change above: one predicate answers both, so a worker and a browser are treated alike for everything except the DOM.
process polyfill that reports a Node version — unenv, behind Nitro and Nuxt,
answers { node: '22.14.0' } — is read as a server, and the three behaviours
above do not apply to it. That trade is deliberate (the alternative misreads real
servers such as Deno and Cloudflare workers, and warns them their secrets have
leaked), and it is the reason a webhook secret or a bot token still does not
belong in code that ships to a client, whatever the SDK detects.If you branch on getEnvironment() yourself, check which question your branch is
asking. === Environment.BROWSE means "there is a DOM", and a worker is correctly
not BROWSE — that branch keeps working. A default: or === Environment.UNKNOWN
arm that was catching workers no longer does. For everything that is not the DOM
— CORS, forbidden headers, whether your code is public — use the new
isBrowserLikeRuntime(), which is true for a browser and a worker alike.
- if (getEnvironment() !== Environment.BROWSE) {
- // "must be a server" — this was wrong in a worker
+ if (!isBrowserLikeRuntime()) {
+ // a server, and only a server
useWebhookSecret()
}
Behaviour: axios settings are filtered, not forwarded whole
httpOptions accepts a named slice of AxiosRequestConfig — TypeHttpOptions — and a key
outside it is dropped at construction, with its name logged (never its
value). A type alone would not do it: excess-property checking does not fire on a
value that arrives through a variable, and plain JavaScript has no compiler at
all. Measured on the unfiltered path: a transformRequest passed this way
replaced the request body wholesale, with no error on either side and a
portal-side failure only.
This also narrows a surface that did ship. HttpV2 and HttpV3 are exported, and
their second constructor argument is typed object — so through 2.x a direct
new HttpV2(auth, { baseURL, responseType, transformResponse, … }) was accepted
by the signature and reached axios untouched. As of 3.0.0 those keys are
dropped. Nothing else changes for callers who build clients the usual way, since
the entry points never forwarded anything before httpOptions existed.
- const http = new HttpV2(auth, { baseURL: 'https://proxy.internal/rest/' })
+ const http = new HttpV2(auth)
+ http.ajaxClient.defaults.baseURL = 'https://proxy.internal/rest/'
headers is the one key that still merges as it did, for that same direct path;
it is not offered on httpOptions.
Behaviour: the JSON content type is stated, not inherited
The SDK has always sent JSON bodies, but never asked for the content type — axios
supplies application/json on its own for a plain-object body. As of 3.0.0 the
SDK sets the header itself, per request.
This matters because the axios instance is public. A Content-Type on
ajaxClient.defaults used to change the encoding of every SDK request, and the
portal reads a filter boolean only from a JSON body: under
application/x-www-form-urlencoded a false arrives as the
string "false", the condition is dropped, and the call answers with rows it
should have excluded — with no error on either side. See
Filtering.
Nothing to change on your side unless you were relying on that default to
re-encode SDK traffic. ajaxClient.defaults.headers no longer reaches it; your
own requests through the same instance are unaffected, FormData included.
Typing: placement.options is no longer any
B24Frame's placement.options used to be typed any on a value
that arrives from the portal across a postMessage boundary. In
3.0.0 it is Readonly<Record<string, unknown>>.
Reading a key still compiles — it yields unknown. What stops
compiling is everything you then do with it, until you narrow. That is the
point: the report behind this change was a condition passed to a slider and read
back as options.payload.id, which compiled without a word and
was undefined at run time.
- const id = $b24.placement.options.payload.id
+ const payload = $b24.placement.options['payload']
+ const id = payload !== null && 'object' === typeof payload
+ ? (payload as { id?: unknown }).id
+ : undefined
Three shapes of break, so you know what to look for:
The last one is the easy one to miss — it moves the error to the call site, not the property.
JSON.parse-ing the value defensively, because one wire shape is
a JSON string. That shape is parsed for you now, so the old advice throws
SyntaxError on an object. TypeScript will not catch this in a
JavaScript codebase — it is the one change here that fails only at run time.Two things get easier, not harder:
optionsis neverundefined. The portal sends an object, a JSON string, or an empty string when the placement carried no parameters; all three are normalised to an object before you see them, so the?.you used to need is now dead weight. (Dead weight, not an error — a defaultstrictproject still compilesoptions?.place.)- A JSON string is parsed for you. The "parse defensively" advice the documentation used to carry is gone with it.
One thing is deliberately lost: five wire shapes now collapse to an empty
object and cannot be told apart afterwards — an absent value, '', a genuinely
empty object, an array, and a string that is not JSON. There is no accessor for
the raw value. If you depend on telling those apart, open an issue rather than
working around it: the shapes that lose information are the ones no known
producer sends.
What does not change: key case is still the portal's choice. For the default placement the object literally is the frame URL's query string, so the keys are whatever the opener wrote; for a registered placement they are whatever the application stored at bind time.
isSliderMode still reads IFRAME === 'Y' — a string, because on
the default-placement path it came from that query string. It does answer
differently in one case: on the JSON-string shape it used to read a property off a
string and return false whatever the value was, and now returns
true when that JSON says "IFRAME":"Y". A boolean
true — which a registered placement can store — still does not
count, by design.
MessageInitData.PLACEMENT_OPTIONS is now
unknown rather than Record<string, any>, which
was never true for the two string shapes. If you read that type directly — rather
than b24.placement.options — narrow it, or better, stop: the
manager exists to do this once.How to find every call site
Through 2.x these symbols announced themselves — a JSSDK_CORE_DEPRECATED_METHOD
warning on every call. In 3.0.0 they are gone, so there is nothing left to warn
you: TypeScript reports each call site as an error, and plain JavaScript does
not complain until the line runs. Either way, find them statically first:
grep -rnE 'callMethod|callListMethod|fetchListMethod|callBatch|callBatchByChunk|LoggerBrowser|LoggerType|placement\.options|PLACEMENT_OPTIONS|getEnvironment|Environment\.' src
Get-ChildItem -Recurse src -Include *.ts,*.js |
Select-String -Pattern 'callMethod|callListMethod|fetchListMethod|callBatch|callBatchByChunk|LoggerBrowser|LoggerType|placement\.options|PLACEMENT_OPTIONS|getEnvironment|Environment\.'
Once that returns nothing — bar the getEnvironment hits, which are a read-and-decide rather than a fix — you are ready for 3.0.0. If you are on TypeScript,
tsc --noEmit after the upgrade is the stronger check — it finds the call sites
a grep would miss, such as one reached through a TypeB24-typed variable.
getEnvironment is in that list for the opposite reason: the compiler will not
help you there. Removed symbols fail to compile; a branch on the environment
still compiles perfectly and simply answers differently, because a worker now
reports Environment.WORKER
instead of falling through to UNKNOWN. Read each hit and decide which question
it is asking — "is there a DOM" (=== Environment.BROWSE, unchanged) or "do the
browser's rules apply" (now isBrowserLikeRuntime()).