- PVSM.RU - https://www.pvsm.ru -
За последние пару лет я сделал с десяток приложений для Маркетплейса Битрикс24 — от простых iframe-панелей до коннекторов мессенджеров и роботов для бизнес-процессов. На PHP, TypeScript и Python. И каждый раз одна и та же история: документация разбросана по пяти сайтам, половина примеров устарела, а реальные подводные камни обнаруживаются только в продакшене.
Эта статья — сборник всего, что я хотел бы знать перед тем, как начать. Не обзор REST API (он и без меня описан), а именно практические нюансы: что работает не так, как написано, что ломается тихо и как сделать приложение, которое будет стабильно работать на сотне порталов.
Прежде чем лезть в код, стоит определиться, какой тип приложения вы делаете. От этого зависит архитектура.
iframe-приложение — самый распространённый тип. Ваш сервер отдаёт HTML-страницу, Б24 показывает её внутри iframe. Может открываться как отдельная страница в левом меню, как вкладка в карточке сделки/контакта или как виджет. Пользователь видит ваш интерфейс прямо внутри Б24.
Коннектор открытых линий (imconnector) — для интеграции мессенджеров. Сообщения из внешнего канала попадают в открытые линии Б24, менеджеры отвечают оттуда — ответ уходит обратно в мессенджер.
Чат-бот — живёт в чатах Б24, отвечает пользователям. Может быть привязан к открытой линии для автоответов клиентам.
Робот / действие бизнес-процесса (bizproc.activity) — появляется в конструкторе роботов. Срабатывает при смене стадии сделки, создании лида и т.д.
Placement — встраивание UI в конкретные места Б24: кнопка в карточке звонка, вкладка в сделке, элемент в меню CRM.
Одно приложение может быть всем сразу: iframe для настроек + коннектор для сообщений + робот для автоматизации. При установке всё регистрируется одним пакетом.
Когда пользователь устанавливает приложение, Б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 абстрагирует детали), но суть та же: нельзя рассчитывать на один формат.
Ответная 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 может установить ваше приложение. У каждого — свои токены, свои данные. Это мультитенантная архитектура. Вопрос: где хранить конфиги?
Самый простой. Для каждого портала создаётся директория с конфигом:
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();
Плюс: просто, работает, не нужна БД. Минус: не скейлится, нет транзакций, при гонке двух запросов файл может повредиться.
Для серьёзных приложений — база данных. Минимальная схема:
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.
Для легковесных приложений (чат-боты, простые роботы) — 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 протухает, и приложение ломается. Единственный способ починить — переустановить.
Но даже в рамках одного часа всё не так просто.
Для облачного Б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. Это накладывает ограничения.
Если используете 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']);
Для работы с контекстом Б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) — по ним меньше всего документации, и большинство тонкостей приходится выяснять методом проб и ошибок.
Автор: ShyDamn
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/iframe/449139
Ссылки в тексте:
[1] Источник: https://habr.com/ru/articles/1020748/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1020748
Нажмите здесь для печати.