v2.2.0

Migration to v3 (legacy REST surface removal)

How to migrate off the deprecated REST surface that is removed in Bitrix24 JS SDK 3.0.0.

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.

These symbols no longer exist. Through 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.
Step-by-step 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:

RemovedReplacement
b24.callMethod(method, params, start)b24.actions.v{2,3}.call.make(options)
b24.callListMethod(method, params, progress, customKeyForResult)b24.actions.v{2,3}.callList.make(options) 1not a drop-in, see below
b24.fetchListMethod(method, params, idKey, customKeyForResult)b24.actions.v{2,3}.fetchList.make(options) 1
b24.callBatch(calls, isHaltOnError, returnAjaxResult)b24.actions.v{2,3}.batch.make(options)
b24.callBatchByChunk(calls, isHaltOnError)b24.actions.v{2,3}.batchByChunk.make(options)
- 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:

3.0.0
result.isMore() / hasMore()kept, restApi:v2 onlyreads next
result.getTotal()kept, restApi:v2 onlyreads total
result.getNext(http) / fetchNext(http)kept, restApi:v2 onlyre-runs the query at the next offset; throws on a restApi:v3 client

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 *.aggregate action and actions.v3.aggregate loses @experimental (a v3 count would then exist in practice, giving getTotal() a replacement for the first time), or when a restApi:v2 sunset date is announced.
  • getNext() / fetchNext() — a restApi:v2 sunset date, and nothing else. They already have a working replacement, so aggregate maturing changes nothing for them.
All five are restApi:v2 only, and always were. Under restApi:v3:
  • getTotal() returns 0 and isMore() returns falsebecause the field is absent, not because the count is zero or the rows ran out. Do not branch on either.
  • getNext() / fetchNext() throw SdkError({ code: 'JSSDK_CORE_METHOD_NOT_SUPPORT_IN_API_V3' }). Deliberately: returning false would 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.

The index signature stays. Only its value type narrowed. Removing [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.
A 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 install prints an EBADENGINE warning, and under engine-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 AxiosRequestConfigTypeHttpOptions — 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
  }
}
Values the SDK cannot order are not called backwards. Numbers are judged whenever both are finite. Strings are judged in only two shapes, the ones where JavaScript's order provably matches the server's: both of equal length, and only as digits (a zero-padded id) or as ISO-8601 datetimes that state the same zone with the same separator.Everything else falls back to the equality check it had before — a cursor whose type changed (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-Agent default 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;
  • TelegramHandler no longer sends from a worker. It warns in the console as it does in a browser, and testConnection() returns false rather 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.

Detection is a safety net, not a boundary. A browser worker bundled with a 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 AxiosRequestConfigTypeHttpOptions — 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:

what you wrotewhat happens now
options.payload.id — reaching througherror: payload is unknown
const s: string = options['place'] — assigning to a concrete typeerror: unknown is not string
helper(options) where helper takes Record<string, string>error: values are unknown

The last one is the easy one to miss — it moves the error to the call site, not the property.

If you were parsing it yourself, stop. The documentation used to advise 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:

  • options is never undefined. 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 default strict project still compiles options?.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.

The wire type moved too: 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

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()).

Footnotes

  1. Pass the correct idKey'ID' for classic list methods, 'id' for crm.item.* / restApi:v3. Omitting it can yield a silently empty result. 2