За последние пару лет я сделал с десяток приложений для Маркетплейса Битрикс24 — от простых iframe-панелей до коннекторов мессенджеров и роботов для бизнес-процессов. На PHP, TypeScript и Python. И каждый раз одна и та же история: документация разбросана по пяти сайтам, половина примеров устарела, а реальные подводные камни обнаруживаются только в продакшене.
Эта статья — сборник всего, что я хотел бы знать перед тем, как начать. Не обзор REST API (он и без меня описан), а именно практические нюансы: что работает не так, как написано, что ломается тихо и как сделать приложение, которое будет стабильно работать на сотне порталов.
Типы приложений: что вообще можно сделать
Прежде чем лезть в код, стоит определиться, какой тип приложения вы делаете. От этого зависит архитектура.
iframe-приложение — самый распространённый тип. Ваш сервер отдаёт HTML-страницу, Б24 показывает её внутри iframe. Может открываться как отдельная страница в левом меню, как вкладка в карточке сделки/контакта или как виджет. Пользователь видит ваш интерфейс прямо внутри Б24.
Коннектор открытых линий (imconnector) — для интеграции мессенджеров. Сообщения из внешнего канала попадают в открытые линии Б24, менеджеры отвечают оттуда — ответ уходит обратно в мессенджер.
Чат-бот — живёт в чатах Б24, отвечает пользователям. Может быть привязан к открытой линии для автоответов клиентам.
Робот / действие бизнес-процесса (bizproc.activity) — появляется в конструкторе роботов. Срабатывает при смене стадии сделки, создании лида и т.д.
Placement — встраивание UI в конкретные места Б24: кнопка в карточке звонка, вкладка в сделке, элемент в меню CRM.
Одно приложение может быть всем сразу: iframe для настроек + коннектор для сообщений + робот для автоматизации. При установке всё регистрируется одним пакетом.
ONAPPINSTALL: первый запрос и первая ловушка
Когда пользователь устанавливает приложение, Б24 делает POST на ваш URL с OAuth-токенами. Казалось бы, всё просто — прочитал req.body.auth, сохранил. Но нет.
Проблема: формат данных
Б24 может прислать токены в разных форматах. Облачная версия шлёт одну структуру, коробочная — другую. При переустановке приложения — третью. Вот реальный код, который обрабатывает все варианты на TypeScript:
router.post('/install', async (req, res) => { let auth = req.body.auth; // Стандартный вложенный объект // Вариант: токены в корне body с другими именами полей if (!auth && (req.body.AUTH_ID || req.body.member_id)) { auth = { member_id: String(req.body.member_id || ''), domain: String(req.query.DOMAIN || req.body.domain || ''), access_token: String(req.body.AUTH_ID || ''), refresh_token: String(req.body.REFRESH_ID || ''), expires_in: parseInt(String(req.body.AUTH_EXPIRES || '3600')), client_endpoint: String( req.body.CLIENT_ENDPOINT || `https://${req.body.domain}/rest/` ), application_token: String(req.body.APPLICATION_TOKEN || ''), }; } // Вариант: данные прямо в body без вложенности if (!auth && req.body.access_token) { auth = req.body; } // Вариант: данные в query (да, бывает) if (!auth && req.query.DOMAIN) { auth = { member_id: String(req.query.member_id || ''), domain: String(req.query.DOMAIN || ''), access_token: String(req.query.AUTH_ID || ''), // ... }; }
На PHP это выглядит проще (потому что CRest SDK абстрагирует детали), но суть та же: нельзя рассчитывать на один формат.
Обязательный BX24.installFinish()
Ответная HTML-страница должна подключить JS-библиотеку Б24 и вызвать BX24.installFinish(). Без этого Б24 считает, что установка не завершена, и приложение будет в статусе «установка»:
<script src="https://api.bitrix24.com/api/v1/"></script> <script> BX24.init(function () { BX24.installFinish(); }); </script>
Частая ошибка — забыть про это и вернуть 200 OK с JSON. Установка вроде бы прошла, токены сохранились, но Б24 не «отпустит» пользователя из экрана установки.
Мультитенантность: один сервер, много порталов
Любой портал Б24 может установить ваше приложение. У каждого — свои токены, свои данные. Это мультитенантная архитектура. Вопрос: где хранить конфиги?
Вариант 1: файловая система (PHP)
Самый простой. Для каждого портала создаётся директория с конфигом:
config/ ├── portal1.bitrix24.ru/config.php ├── portal2.bitrix24.ru/config.php └── portal3.bitrix24.ru/config.php
При установке определяем домен портала и сохраняем туда токены. При запросе — читаем конфиг по домену из рефера:
$portal = Client::getPortalFromUrl($_SERVER['HTTP_REFERER']); $bitrix = new Client($portal, $data); $bitrix->refreshToken();
Плюс: просто, работает, не нужна БД. Минус: не скейлится, нет транзакций, при гонке двух запросов файл может повредиться.
Вариант 2: PostgreSQL (TypeScript, Python)
Для серьёзных приложений — база данных. Минимальная схема:
CREATE TABLE IF NOT EXISTS clients ( id SERIAL PRIMARY KEY, member_id VARCHAR(255) UNIQUE NOT NULL, -- уник. ID портала domain VARCHAR(255) NOT NULL, access_token TEXT NOT NULL, refresh_token TEXT NOT NULL, expires_at TIMESTAMP NOT NULL, application_token VARCHAR(255), client_endpoint VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
member_id — уникальный идентификатор портала на сервере авторизации. По нему определяете, от какого портала пришёл запрос. Все связанные сущности привязываются к client_id через foreign key.
Вариант 3: SQLite (Python)
Для легковесных приложений (чат-боты, простые роботы) — SQLite. Без отдельного сервера БД:
class Database: def __init__(self, path: str): self._path = path conn = connect(self._path, timeout=60.0) cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS bitrix_auth_settings ( application_token TEXT NOT NULL, settings_key VARCHAR(10) NOT NULL, UNIQUE(settings_key) ) """) conn.commit() conn.close()
Достаточно для приложений с небольшой нагрузкой. Не подходит, если ожидаете параллельные запросы от десятков порталов.
Обновление токенов: самый коварный баг
Токен доступа живёт 1 час. Refresh token — 28 дней. Если за 28 дней никто не зашёл в ваше приложение — refresh token протухает, и приложение ломается. Единственный способ починить — переустановить.
Но даже в рамках одного часа всё не так просто.
Проблема: URL для обновления
Для облачного Б24 на .bitrix24.ru обновление идёт через https://oauth.bitrix24.ru/oauth/token/. Но иногда этот URL не работает и нужен https://oauth.bitrix24.tech/oauth/token/. Для коробочной версии — через домен самого портала. И ещё некоторые конфигурации принимают только GET, а другие — только POST.
Рабочий подход — перебор:
function getOAuthTokenUrls(portalDomain?: string): string[] { const list: string[] = []; if (portalDomain) { const domain = portalDomain.replace(/^https?:\/\//, '').split('/')[0]; if (domain.endsWith('bitrix24.ru')) { list.push('https://oauth.bitrix24.tech/oauth/token/'); list.push('https://oauth.bitrix24.ru/oauth/token/'); } list.push(`https://${domain}/oauth/token/`); } return list; } // Для каждого URL пробуем GET и POST for (const url of urlsToTry) { for (const useGet of [true, false]) { const result = await tryRefresh(url, useGet, params); if ('auth' in result) { await db.updateClientTokens(memberId, result.auth); return result.auth; } } }
Превентивное обновление
Не ждите, пока токен протухнет. Обновляйте заранее — за 5 минут до истечения:
export async function getValidAccessToken(memberId: string): Promise<string> { const client = await db.getClientByMemberId(memberId); const now = new Date(); const expiresAt = new Date(client.expires_at); const bufferMs = 5 * 60 * 1000; // 5 минут запаса if (now.getTime() + bufferMs >= expiresAt.getTime()) { const newAuth = await refreshTokens(memberId, client.refresh_token, client.domain); return newAuth.access_token; } return client.access_token; }
А чтобы не потерять refresh token за 28 дней — заведите cron, который обходит все порталы и обновляет протухающие токены.
iframe: ваш UI внутри Б24
Приложение отображается в iframe. Это накладывает ограничения.
Заголовки безопасности
Если используете helmet() в Express — он по умолчанию ставит X-Frame-Options: DENY. Ваше приложение не откроется в Б24:
app.use(helmet({ contentSecurityPolicy: false, crossOriginEmbedderPolicy: false, })); // Явно убираем X-Frame-Options app.use((req, res, next) => { res.removeHeader('X-Frame-Options'); next(); });
Определение портала
Внутри iframe вам доступны query-параметры или данные из POST. Домен портала можно вытащить из HTTP_REFERER (PHP) или из req.query.DOMAIN (JS). Это нужно, чтобы определить, к какому клиенту относится запрос:
$portal = Client::getPortalFromUrl($_SERVER['HTTP_REFERER']);
JS-библиотека BX24
Для работы с контекстом Б24 из фронтенда подключайте:
<script src="https://api.bitrix24.com/api/v1/"></script>
Через BX24 можно получить данные текущего пользователя, сделки, контакта — и выполнять REST-вызовы с автоматической авторизацией, без ручного управления токенами.
Регистрация сущностей при установке
В зависимости от типа приложения, при установке нужно зарегистрировать разные вещи.
Коннектор открытых линий
await callBitrixMethod(clientEndpoint, 'imconnector.register', { ID: 'my_connector', NAME: 'My Connector', ICON: { DATA_IMAGE: '<svg>...</svg>', COLOR: '#25D366' }, PLACEMENT_HANDLER: `${APP_URL}/placement`, }, accessToken);
Подписка на события
const events = [ 'OnImConnectorMessageAdd', 'OnImConnectorMessageDelete', ]; for (const event of events) { try { await callBitrixMethod(clientEndpoint, 'event.bind', { event, handler: `${APP_URL}/webhooks/bitrix`, }, accessToken); } catch (error) { // ВАЖНО: event.bind кидает ошибку при дубликате if (isAlreadyBoundError(error)) continue; throw error; } }
event.bind не возвращает false при повторной привязке — он выбрасывает ошибку. При переустановке приложения события уже привязаны, и без обработки дубликатов установка упадёт.
Робот (действие бизнес-процесса)
await callBitrixMethod(clientEndpoint, 'bizproc.activity.add', { CODE: 'my_robot', HANDLER: `${APP_URL}/bizproc/robot`, NAME: { ru: 'Моё действие' }, DESCRIPTION: { ru: 'Описание действия' }, PROPERTIES: { message: { Name: { ru: 'Сообщение' }, Type: 'string', Required: 'Y' }, }, USE_PLACEMENT: 'Y', PLACEMENT_HANDLER: `${APP_URL}/bizproc/placement`, }, accessToken);
После выполнения робот обязан вернуть результат через bizproc.event.send — иначе бизнес-процесс зависнет:
await callBitrixMethod(endpoint, 'bizproc.event.send', { EVENT_TOKEN: eventToken, RETURN_VALUES: { status: 'ok' }, LOG_MESSAGE: 'Действие выполнено', });
Очереди и фоновая обработка
Некоторые задачи нельзя выполнить за один HTTP-запрос. Пример: обновить поля у всех контактов портала (тысячи записей). REST API отдаёт по 50 элементов за вызов, лимит — 2 запроса в секунду. Тысяча контактов = минуты работы.
Решение — очереди. Пользователь нажимает кнопку → приложение создаёт файл задачи → cron подхватывает и обрабатывает порциями:
// Пользователь нажал "Запустить обработку" $contact->createProcessFile($portal, $categories); // Создаётся файл queue/portal.bitrix24.ru__contact.json // Cron (каждую минуту): $queue = array_diff(scandir($dir), ['..', '.']); $file = array_values($queue)[0]; $data = json_decode(file_get_contents("$dir/$file"), true); $result = $contact->process($data); if ($result['finish']) { unlink("$dir/$file"); // Уведомляем пользователя через им-нотификацию $bitrix->request('im.notify.system.add', [ 'USER_ID' => $data['user'], 'MESSAGE' => 'Обработка завершена' ]); } else { // Сохраняем прогресс и продолжим на следующем тике $data['start'] = $result['last']; file_put_contents("$dir/$file", json_encode($data)); }
Для TypeScript/Python — аналогично, но вместо файлов можно использовать Redis или PostgreSQL как очередь.
Важный нюанс: по завершении длительной операции уведомляйте пользователя через im.notify.system.add. Иначе он не узнает, что задача готова.
Подводные камни: сборник граблей
Собрал за годы разработки. Каждый пункт — реальный баг в продакшене.
event.bind кидает ошибку при дубликате. Не { result: false }, а HTTP-ошибку. Ловите и игнорируйте при переустановке.
OAuth URL для облака и коробки отличается. .bitrix24.ru → oauth.bitrix24.ru или oauth.bitrix24.tech. Коробка → домен портала. GET vs POST — тоже отличается. Перебирайте варианты.
Refresh token протухает за 28 дней. Если приложение не обновляет токен 28 дней — оно ломается. Заведите cron для превентивного обновления.
imconnector.send.messages не возвращает ID отправленного сообщения. Если потом нужно удалить/обновить — ведите свой маппинг.
document_id в роботах приходит в разных форматах. LEAD_67, crm,CCrmDocumentLead,LEAD_67, 67. Парсите все:
function parseCrmDocumentId(documentId: string): string { const s = String(documentId || '').trim(); const numOnly = s.replace(/\D/g, ''); if (numOnly) return numOnly; const parts = s.split(','); const last = parts[parts.length - 1] || ''; const match = last.match(/^(?:LEAD|DEAL)_(\d+)$/i) || last.match(/(\d+)$/); return match ? match[1] : s; }
Rate limit: 2 запроса в секунду. На batch-операциях упираетесь мгновенно. Используйте batch (до 50 команд за вызов) и очереди.
iframe и helmet(). По умолчанию helmet ставит X-Frame-Options: DENY. Приложение не откроется в Б24. Отключайте CSP и X-Frame-Options для iframe-маршрутов.
application_token нужно проверять. Б24 присылает его при установке и при каждом событии. Сравнивайте — это защита от подделки.
Данные из формы installation могут прийти и в POST, и в GET, и в query. При переустановке формат может отличаться от первой установки. Проверяйте все источники.
bizproc.event.send обязателен. Если робот не вернул результат — бизнес-процесс зависает навсегда. Оборачивайте в try/catch и возвращайте event.send даже при ошибке.
Петля сообщений в коннекторах. Менеджер ответил → вы отправили в мессенджер → мессенджер прислал вебхук → вы попытались отправить обратно в Б24 → бесконечный цикл. Решается кэшем отправленных сообщений с TTL.
Чеклист перед модерацией
HTTPS — обязательно, самоподписанные не принимаются
BX24.installFinish()— страница установки должна его вызватьПереустановка — приложение не должно падать при повторной установке (дубликаты в БД, event.bind)
Refresh token — автоматическое обновление, cron для протухающих
Права — запрашивайте только те scope, которые реально используете
Мультиклиентность — работа с несколькими порталами одновременно
Обработка ошибок — приложение не должно показывать 500 при невалидных данных
Уведомления — длительные операции должны уведомлять пользователя о завершении
Вопросы по нюансам разработки — в комментариях. Особенно приветствую вопросы про коннекторы мессенджеров (imconnector) и роботы (bizproc.activity) — по ним меньше всего документации, и большинство тонкостей приходится выяснять методом проб и ошибок.
