Как делать приложения для Маркетплейса Битрикс24 и не сойти с ума: подводные камни, которых нет в документации

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

За последние пару лет я сделал с десяток приложений для Маркетплейса Битрикс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.


Чеклист перед модерацией

  1. HTTPS — обязательно, самоподписанные не принимаются

  2. BX24.installFinish() — страница установки должна его вызвать

  3. Переустановка — приложение не должно падать при повторной установке (дубликаты в БД, event.bind)

  4. Refresh token — автоматическое обновление, cron для протухающих

  5. Права — запрашивайте только те scope, которые реально используете

  6. Мультиклиентность — работа с несколькими порталами одновременно

  7. Обработка ошибок — приложение не должно показывать 500 при невалидных данных

  8. Уведомления — длительные операции должны уведомлять пользователя о завершении


Вопросы по нюансам разработки — в комментариях. Особенно приветствую вопросы про коннекторы мессенджеров (imconnector) и роботы (bizproc.activity) — по ним меньше всего документации, и большинство тонкостей приходится выяснять методом проб и ошибок.

Источник: https://habr.com/ru/articles/1020748/


Интересные статьи

Интересные статьи

В опенсорс-проектах часто можно увидеть использование инструментов для проверки кода: проверяется кодстайл, выполняется статический анализ. Эти инструменты широко распространены, но в проектах на Битр...
2 февраля 2024 года приложение «Учёт Онлайн» удалили из App Store. Это произошло спустя двое суток после публикации аналога «Сбербанк Онлайн» в магазине приложений Apple.
Практически в каждой компании есть корпоративная система поощрений. Но вот как прописать для неё правила, да ещё и на Битриксе, — вопрос. Делюсь личным опытом.Привет! Я full-stack веб-разработчик в IB...
Если Вы используете Битрикс24 и у Вас один из архивных тарифов, то наверняка Вы уже в курсе того, что в 2023 году планируется прекратить их поддержку. Если нет, то сейчас самое время чтобы об этом пог...
Если вы являетесь регулярным читателем Хабра, то должно быть заметили что за последние несколько лет вышло немало статей о сборе персональных данных с мобильных устройств...