These managers exist only on B24Frame. None of them work for B24Hook or B24OAuth. Always init via:
import { initializeB24Frame } from '@bitrix24/b24jssdk'
const $b24 = await initializeB24Frame()
…and call $b24.destroy() when the page/component unmounts.
Slider — open paths and app pages
// 1. Open a portal path in a slider
const url = $b24.slider.getUrl('/crm/deal/details/1')
const status = await $b24.slider.openPath(url, /* width */ 1640)
if (status.isOpenAtNewWindow) { // Mobile fallback: it opened a new tab and polled for close }
// 2. Open YOUR app's page as a slider. Settles with '' when it is CLOSED — don't await to "open" $b24.slider.openSliderAppPage({ some: 'param' }).then(() => { /* the slider was closed */ }).catch(() => {})
// Inside the slider: closing from code never settles, and on a cloud portal it // left the page unclickable (#486). Prefer the portal's ✕. $b24.slider.closeSliderAppPage().catch(() => {})
`openPath` returns when the slider closes (or the popup is closed on mobile). Always check `isOpenAtNewWindow` if you depend on close-detection.
`openPath` opens a **portal** path; `openSliderAppPage` re-opens **your own registered handler URL**. Pointing `openPath` at an application page yields a 404 on the portal side.
`openSliderAppPage` settles on **close**, never on open, always with `''` (measured, #486). It never settles if the calling page reloads while the slider is open.
Everything you pass to `openSliderAppPage` is forwarded to the new frame as `PLACEMENT_OPTIONS`, so `place` is an ordinary call parameter — **not** a registered placement, and no `placement.bind` is needed. `bx24_width` below your own mobile breakpoint silently renders the mobile layout.
### Routing the opened slider to the right screen (Nuxt)
The SDK's job ends when the parameter is delivered; routing is yours. In Nuxt, **do not just return `navigateTo()` from route middleware** — that redirect is silently discarded when the entry route is prerendered, which `nuxt build` does for anything in `nitro.prerender.routes` (and, with `crawlLinks`, anything reachable by link). The portal opens the frame with a query string, and for a prerendered page Nuxt hydrates on the bare path and then restores the original URL on `app:suspense:resolve`, bypassing guards. Nothing reports an error; the slider just shows your main page.
```ts
export default defineNuxtRouteMiddleware(async (to) => {
if (import.meta.server) return
const { $initializeB24Frame } = useNuxtApp()
const $b24 = await $initializeB24Frame()
// `options` values are `unknown` — the portal decides what it puts there, so
// narrow before passing it on. This is the anti-pattern the skill warns about.
const rawPlace = $b24.placement.options['place']
const target = routeForPlace('string' === typeof rawPlace ? rawPlace : '')
// isSamePath must tolerate a trailing slash: a static server serves `/app/`
// while the router resolves `/app`, and a strict !== cancels every redirect.
if (!target || isSamePath(to.path, target)) return
// Query from the ADDRESS BAR, not `to.query` — a prerendered page hydrates on
// the bare path, so `to` has no query and the target would lose `place`.
const dest = { path: target, query: Object.fromEntries(new URLSearchParams(location.search)) }
const nuxtApp = useNuxtApp()
if (!nuxtApp.isHydrating) return navigateTo(dest, { replace: true })
onNuxtReady(() => {
void nuxtApp.runWithContext(async () => {
const router = useRouter()
if (!isSamePath(router.currentRoute.value.path, to.path)) return
// router.replace, not navigateTo: navigateTo RETURNS a route object when it
// catches another navigation in progress, and nobody would apply it.
// Check the result — a failed navigation resolves, it does not throw.
const failure = await router.replace(dest)
if (failure) console.warn('slider redirect failed', failure.type)
})
})
})
onNuxtReady works because it hooks the same app:suspense:resolve but subscribes later and defers to requestIdleCallback, running after the restore. app:mounted is too early — it fails with NavigationFailure: aborted. redirectCode is server-only, abortNavigation cannot redirect, and external: true means a full reload.
Dialog — pick users
const single = await $b24.dialog.selectUser()
// single: { id, name, lastName, photo, … } | null
const many = await $b24.dialog.selectUsers()
// many: SelectedUser[]
Dialog — pick CRM entities (selectCRM)
Active method (re-implemented in packages/jssdk/src/frame/dialog.ts:175-235). Returns an object with per-entity-type buckets, each a real Array so you can use .length/.map()/for..of. Buckets for entity types you did not request are undefined.
import type { SelectCRMParams, SelectedCRM } from '@bitrix24/b24jssdk'
// Pick one contact
const single = await $b24.dialog.selectCRM({
entityType: ['contact'],
multiple: false
})
const contact = single.contact?.[0]
// contact: { id: 'C_<n>', title, image, … } | undefined
// Pick multiple deals and companies, with pre-selected values
const picked: SelectedCRM = await $b24.dialog.selectCRM({
entityType: ['deal', 'company'],
multiple: true,
// Numeric ids, not the prefixed `D_42` / `CO_7` form the dialog returns.
value: { deal: [42], company: [7] }
})
for (const deal of picked.deal ?? []) {
console.log(deal.id, deal.title) // id type-narrows to `D_${number}`
}
The id format is per entity:
- lead →
L_<n> - contact →
C_<n> - company →
CO_<n> - deal →
D_<n> - quote →
Q_<n>
Dialog — pick access targets (selectAccess)
// The argument is a string[] of permission ids to block, not an options object.
const access = await $b24.dialog.selectAccess([])
Less commonly used. Returns the parent window's raw access-selection payload — refer to Bitrix24's selectAccess docs for the shape.
Parent — control the iframe in the portal layout
await $b24.parent.fitWindow() // shrink-wrap to content
await $b24.parent.resizeWindow(800, 600) // explicit size
await $b24.parent.resizeWindowAuto(rootEl, /* minH */ 400, /* minW */ 300)
await $b24.parent.setTitle('My App') // in-page #pagetitle, NOT the browser tab
await $b24.parent.scrollParentWindow(0)
await $b24.parent.reloadWindow()
// Closing: save first, then close without awaiting (#328).
await $b24.options.appSet('draft', 'value')
$b24.parent.closeApplication().catch(() => {})
setTitle changes the in-page #pagetitle, not the browser tab — to set the browser tab title, open a slider via slider.openSliderAppPage({ bx24_title: '…' }).
For IM:
await $b24.parent.imCallTo(/* userId */ 5, /* video */ true)
await $b24.parent.imOpenMessenger('chat12')
await $b24.parent.imPhoneTo('+70000000000')
await $b24.parent.imOpenHistory('chat12')
All four are fire-and-forget. The portal's handlers take no callback, so
nothing ever answers: the promise resolves on the SDK's own short timer and means
the command was posted, not that the call started or the chat opened. Never
branch on it as a success signal, and do not treat the action im… stop by timeout log line that follows as a fault — it is the normal outcome.
Calling any of them prints a deprecation notice in the portal's console
recommending Messenger.startPhoneCall / startVideoCall / openChat. Those are
top-window methods and are not reachable from an application — they are not
part of a placement's command vocabulary. The notice comes from the portal's own
compatibility layer, which these methods reach anyway; it cannot be avoided, and
there is nothing to migrate to. See #331.
imPhoneTo accepts a second params argument and imOpenMessenger a second
messageId, mirroring the portal's own startPhoneCall(number, params) and
openChat(dialogId, messageId). The portal does not forward either today, so
they have no effect yet — send them only so that no change is needed when it does.
Placement — context the user opened your app from
$b24.placement.title // string identifier of the placement
$b24.placement.options // params passed by the placement; Readonly<Record<string, unknown>>, never undefined
$b24.placement.isSliderMode // boolean
const iface = await $b24.placement.getInterface()
// React to events from the host page
await $b24.placement.bindEvent('onCrmEntityUpdate', (...args) => {
console.log('host event:', args)
})
// Call host-defined commands
await $b24.placement.call('someCommand', { foo: 'bar' })
await $b24.placement.callCustomBind('someCommand', { opt: 1 }, (...args) => {
// result callback
})
placement.setValue — send a value back to the host placement
Use setValue when your iframe is mounted in a "select-value" placement (e.g. a custom CRM-field selector). The host expects a JSON-serialized string and calls JSON.parse on receipt.
// Convenience helper that serializes for you (preferred)
await $b24.placement.setValue('plain string')
await $b24.placement.setValue({ id: 1, title: 'demo' })
await $b24.placement.setValue([1, 2, 3])
Manual form via placement.call('setValue', …) is also supported, but value MUST already be a JSON-serialized string — the SDK throws TypeError otherwise (see packages/jssdk/src/frame/placement.ts:104-113):
// Equivalent but requires you to stringify yourself
await $b24.placement.call('setValue', { value: JSON.stringify({ id: 1 }) })
Options — persist app and per-user settings
// App-level (visible to all users of this app on the portal)
await $b24.options.appSet('installComplete', true)
const installed = $b24.options.appGet('installComplete')
// User-level (per-user on this portal)
await $b24.options.userSet('theme', 'dark')
const theme = $b24.options.userGet('theme')
Values are JSON-serialized server-side. Read returns the parsed value.
For batched/structured options, prefer the helper-level options manager (see b24jssdk-helpers).
Auth — env, refresh, language
const auth = $b24.auth.getAuthData()
// { access_token, refresh_token, expires_in, domain, member_id } | false
if (!auth) {
await $b24.auth.refreshAuth()
}
const lang = $b24.getLang() // 'ru' | 'en' | …
const sid = $b24.getAppSid()
The SDK auto-refreshes auth on 401 — manual refreshAuth() is rarely needed.
Concurrent refreshAuth() calls are coalesced into one message to the parent window.
Token keep-alive — for apps that use the token outside the SDK
The auto-refresh runs only on the request path: an already-expired token is
renewed before a call (a valid one is used as is), and again on a 401.
An app that reads the token and sends it to its own backend makes no $b24
call, so nothing ever refreshes it: after AUTH_EXPIRES seconds of an idle tab
the token is gone and the app's own requests start answering 401.
import { initializeB24Frame } from '@bitrix24/b24jssdk'
// Opt in — off by default, and pass it at the app's FIRST init (the first
// caller's options win). Refreshes ahead of expiry, re-checks when the tab
// becomes visible again, backs off when the parent window refuses. Costs no
// REST call (it is a postMessage), never throws at the app, stops with
// $b24.destroy().
const $b24 = await initializeB24Frame({ keepAuthFresh: true })
// Tune it if the portal hands out short-lived tokens:
// keepAuthFresh: { marginMs: 120_000, minDelayMs: 30_000, maxDelayMs: 120_000 }
Turn it on when the token leaves the SDK, or when the page sits open and idle.
Leave it off when every request goes through $b24 — Bitrix warns that
refreshing too often risks an app being auto-blocked.
Lifecycle template (Vue 3)
import { onMounted, onBeforeUnmount, ref } from 'vue'
import { initializeB24Frame, type B24Frame } from '@bitrix24/b24jssdk'
const $b24 = ref<B24Frame | null>(null)
onMounted(async () => {
$b24.value = await initializeB24Frame()
await $b24.value.parent.setTitle('My App')
await $b24.value.parent.fitWindow()
})
onBeforeUnmount(() => {
$b24.value?.destroy()
})
Install mode — finish it, or the portal sends you nothing
An application is "installing" from the moment the user accepts the install dialog
until it calls installFinish(). Until that call lands, the portal treats the app
as half-installed and delivers no events to it — bot handlers, handlers
registered with event.bind, and other outgoing calls never reach your endpoints.
The failure is silent: registration succeeds, imbot.register returns a botId,
the handlers are listed when queried, and no traffic arrives. Generated code must
therefore always finish the install flow.
import { initializeB24Frame } from '@bitrix24/b24jssdk'
const $b24 = await initializeB24Frame()
if ($b24.isInstallMode) {
// provision fields, register placements, seed options...
// Last statement of the flow: the portal reloads the page in response,
// so anything after this may never run.
await $b24.installFinish()
}
The opposite holds for an API-only app with no installation interface: it must
not call installFinish(). Its installation completes automatically, and the
method works only inside a browser interface frame — so gate the call on
isInstallMode, which is only ever true inside that frame.
Finishing the installation is a lifecycle step, not a security one. An app that
also runs an OAuth install/uninstall endpoint still has to verify application_token
on ONAPPINSTALL / ONAPPUNINSTALL — see the Security patterns
page. Doing one does not cover the other.
To check an app the portal is ignoring, ask whether it considers it installed:
app.info returns INSTALLED, and false means installFinish() never landed.
Anti-patterns
- ❌ Using
B24Frameoutside a Bitrix24 placement —initializeB24Frame()will hang waiting for the parent handshake. UseB24Hookfor non-frame contexts. - ❌ Calling slider / dialog APIs before
await initializeB24Frame()resolves. - ❌ Provisioning an app in install mode and never calling
installFinish()— the app stays half-installed and the portal delivers no events to it, with nothing failing loudly to say so. - ❌ Putting work after
await installFinish()— the portal reloads the page in response, so it may never run. - ❌
$b24.placement.call('setValue', { value: { id: 1 } })— throws becausevalueis not a string. Use$b24.placement.setValue({ id: 1 })or stringify yourself. - ❌ Storing secrets in
options.appSet— placement options are visible to everyone with access to the placement. - ❌ Treating a resolved
parent.im*promise as proof the call started or the chat opened — nothing answers those commands; the promise only means the message was posted. - ❌ Firing
parent.closeApplication()without.catch(() => {})and calling$b24.destroy()in the same breath —destroy()rejects every in-flight command withJSSDK_FRAME_DISPOSED, and a discarded promise becomes an unhandled rejection. Attach a.catch. - ❌
await parent.closeApplication()— on some builds the portal never answers, so it waits forever (#328). Cleanup first, then close without awaiting. - ❌ Treating an absent
selectCRMbucket as an empty array — they areundefined. Usepicked.deal ?? []. - ❌
return navigateTo(target)from Nuxt route middleware to route an opened slider — discarded without error on a prerendered entry route. Navigate fromonNuxtReadywhile hydrating. - ❌ Trusting
$b24.placement.options['place']without narrowing it — values areunknown, because the data crosses a postMessage boundary from the portal. Key case is the portal's choice too: for the default placement the object is the frame URL query string. (The empty-string and absent wire shapes are normalised by the SDK since 3.0.0 (a JSON string is handled too, though it belongs to the form-resubmit path and was not observed on the handshake), so?.is no longer needed.) - ❌ Gating slider diagnostics on
placement.isSliderMode— it is derived fromPLACEMENT_OPTIONS.IFRAME, so it goes quiet exactly when the placement data you are diagnosing is missing.