Уведомления о заказах в Telegram — одна из тех доработок, которые заказчик замечает сразу и хвалит. Делается она за вечер, и ровно поэтому в ней часто остаются две дыры: публичный эндпоинт вебхука без проверки подлинности и выдача данных о заказе любому, кто угадал его номер.
Разберём рабочую интеграцию и оба этих места отдельно — они важнее, чем сам код отправки сообщений.
Зачем Telegram-бот для интернет-магазина
Telegram — один из самых популярных мессенджеров в России. Интеграция с ботом даёт:
- Мгновенные уведомления — менеджер узнаёт о заказе за секунды, а не когда откроет почту
- Двусторонняя связь — клиент пишет в бот, менеджер отвечает из админки
- Автоматизация — статус заказа, трек-номер, напоминания без участия человека
Что получите в итоге: Работающего бота, который отправляет уведомления о новых заказах в Telegram-группу и позволяет клиентам узнать статус заказа по номеру.
Шаг 1: Создание бота в BotFather
Откройте @BotFather в Telegram и создайте бота:
/newbot
# Введите имя: МойМагазин Уведомления
# Введите username: myshop_notify_bot
# BotFather вернёт токен:
# 7234567890:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsawСохраните токен — он понадобится для API-запросов.
Токен бота — полноценный доступ к боту. Кто им владеет, тот может читать все входящие сообщения и писать от имени бота всем, кто с ним общался. Держите его в конфигурации вне репозитория (Configuration::getValue() или переменная окружения), а не в константе init.php, которая уедет в git. Если токен всё-таки засветился — отзывайте через /revoke у BotFather, смена пароля тут не поможет.
Шаг 2: Модуль интеграции в Битрикс
Создадим простой модуль для отправки сообщений. Структура:
local/
├── php_interface/
│ └── init.php
└── classes/
└── Telegram/
└── Bot.phpКласс для работы с Telegram API
<?php
// local/classes/Telegram/Bot.php
namespace Local\Telegram;
class Bot
{
private string $token;
private string $apiUrl = 'https://api.telegram.org/bot';
public function __construct(string $token)
{
$this->token = $token;
$this->apiUrl .= $token . '/';
}
/**
* Отправка сообщения в чат
*/
public function sendMessage(
int|string $chatId,
string $text,
array $options = []
): array {
$params = array_merge([
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => 'HTML',
], $options);
return $this->request('sendMessage', $params);
}
/**
* Отправка сообщения с inline-кнопками
*/
public function sendMessageWithButtons(
int|string $chatId,
string $text,
array $buttons
): array {
return $this->sendMessage($chatId, $text, [
'reply_markup' => json_encode([
'inline_keyboard' => $buttons
])
]);
}
/**
* Базовый запрос к API
*/
private function request(string $method, array $params = []): array
{
$ch = curl_init($this->apiUrl . $method);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
]);
$response = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
throw new \Exception("Telegram API error: $error");
}
return json_decode($response, true);
}
}Подключение в init.php
<?php
// local/php_interface/init.php
use Bitrix\Main\Loader;
use Bitrix\Main\EventManager;
// Автозагрузка классов
Loader::registerAutoLoadClasses(null, [
'Local\\Telegram\\Bot' => '/local/classes/Telegram/Bot.php',
]);
// Токен бота (лучше хранить в .settings.php)
define('TELEGRAM_BOT_TOKEN', '7234567890:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw');
define('TELEGRAM_CHAT_ID', '-1001234567890'); // ID группы для уведомленийШаг 3: Уведомления о новых заказах
Подписываемся на событие создания заказа:
<?php
// local/php_interface/init.php (продолжение)
EventManager::getInstance()->addEventHandler(
'sale',
'OnSaleOrderSaved',
['Local\\Events\\OrderHandler', 'onOrderSaved']
);<?php
// local/classes/Events/OrderHandler.php
namespace Local\Events;
use Bitrix\Main\Event;
use Bitrix\Sale\Order;
use Local\Telegram\Bot;
class OrderHandler
{
public static function onOrderSaved(Event $event): void
{
/** @var Order $order */
$order = $event->getParameter('ENTITY');
// Только для новых заказов
if (!$event->getParameter('IS_NEW')) {
return;
}
self::sendNewOrderNotification($order);
}
private static function sendNewOrderNotification(Order $order): void
{
$bot = new Bot(TELEGRAM_BOT_TOKEN);
// Собираем данные заказа
$orderId = $order->getId();
$price = number_format($order->getPrice(), 0, ',', ' ');
$currency = $order->getCurrency();
// Данные покупателя
$props = $order->getPropertyCollection();
$phone = $props->getPhone()?->getValue() ?? '—';
$name = $props->getPayerName()?->getValue() ?? '—';
// Товары
$basket = $order->getBasket();
$items = [];
foreach ($basket as $item) {
$items[] = "• {$item->getField('NAME')} × {$item->getQuantity()}";
}
$itemsList = implode("\n", array_slice($items, 0, 5));
if (count($items) > 5) {
$itemsList .= "\n... и ещё " . (count($items) - 5) . " поз.";
}
// Формируем сообщение
$message = "🛒 <b>Новый заказ #{$orderId}</b>\n\n"
. "💰 Сумма: <b>{$price} {$currency}</b>\n"
. "👤 {$name}\n"
. "📱 {$phone}\n\n"
. "📦 Состав:\n{$itemsList}";
// Кнопка для перехода в админку
$buttons = [[
[
'text' => '📋 Открыть заказ',
'url' => $_SERVER['HTTP_HOST']
. "/bitrix/admin/sale_order_view.php?ID={$orderId}"
]
]];
try {
$bot->sendMessageWithButtons(
TELEGRAM_CHAT_ID,
$message,
$buttons
);
} catch (\Exception $e) {
// Логируем ошибку, но не ломаем оформление заказа
\Bitrix\Main\Diag\Debug::writeToFile(
$e->getMessage(),
'Telegram notification error'
);
}
}
}Важно: Никогда не прерывайте оформление заказа из-за ошибки отправки в Telegram. Оборачивайте отправку в try-catch и логируйте ошибки.
Шаг 4: Webhook для входящих сообщений
Чтобы бот мог отвечать на сообщения, настроим webhook:
<?php
// local/tools/telegram_webhook.php
use Bitrix\Main\Loader;
use Local\Telegram\Bot;
require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';
Loader::includeModule('sale');
// Получаем данные от Telegram
$input = file_get_contents('php://input');
$update = json_decode($input, true);
if (!$update || !isset($update['message'])) {
exit;
}
$message = $update['message'];
$chatId = $message['chat']['id'];
$text = $message['text'] ?? '';
$bot = new Bot(TELEGRAM_BOT_TOKEN);
// Обработка команд
if (str_starts_with($text, '/start')) {
$bot->sendMessage($chatId,
"👋 Привет! Я бот магазина.\n\n"
. "Отправьте номер заказа, чтобы узнать его статус.\n"
. "Например: <code>12345</code>"
);
exit;
}
// Проверка статуса заказа по номеру
if (preg_match('/^\d+$/', trim($text))) {
$orderId = (int) trim($text);
$order = \Bitrix\Sale\Order::load($orderId);
// Проверяем не только существование заказа, но и право на него:
// без этого любой желающий перебором номеров выгрузит статусы
// и трек-номера всех заказов магазина
if (!$order || !isOrderOwnedByChat($order, $chatId)) {
$bot->sendMessage($chatId, "Заказ #{$orderId} не найден");
exit;
}
$status = $order->getField('STATUS_ID');
$statusName = \Bitrix\Sale\Internals\StatusLangTable::getList([
'filter' => ['STATUS_ID' => $status, 'LID' => 'ru'],
'select' => ['NAME']
])->fetch()['NAME'] ?? $status;
$shipments = $order->getShipmentCollection();
$trackNumber = '';
foreach ($shipments as $shipment) {
if (!$shipment->isSystem()) {
$trackNumber = $shipment->getField('TRACKING_NUMBER') ?: '';
break;
}
}
$response = "📦 <b>Заказ #{$orderId}</b>\n\n"
. "Статус: {$statusName}\n";
if ($trackNumber) {
$response .= "Трек-номер: <code>{$trackNumber}</code>";
}
$bot->sendMessage($chatId, $response);
exit;
}
// Неизвестная команда
$bot->sendMessage($chatId,
"🤔 Не понял. Отправьте номер заказа цифрами."
);Регистрация webhook
curl -X POST "https://api.telegram.org/bot<TOKEN>/setWebhook" \
-d "url=https://your-site.ru/local/tools/telegram_webhook.php" \
-d "secret_token=$(openssl rand -hex 32)"Значение secret_token Telegram будет присылать в заголовке X-Telegram-Bot-Api-Secret-Token с каждым обновлением. Первое, что должен делать обработчик, — сверять его:
$secret = (string) ($_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? '');
if (!hash_equals(\Bitrix\Main\Config\Configuration::getValue('telegram_webhook_secret'), $secret)) {
http_response_code(403);
exit;
}Без этой проверки ваш вебхук — открытый эндпоинт. Адрес скрипта не секрет: он попадает в логи, в историю команд, его несложно подобрать по типовым путям. Кто угодно может отправить туда поддельное «обновление» с любым chat_id и любым текстом — и заставить вашего бота выполнять команды от имени несуществующего пользователя.
Это самая серьёзная ошибка в типовых реализациях такого бота. В исходном виде обработчик отдаёт статус и трек-номер заказа любому, кто пришлёт его номер. Номера заказов последовательны, поэтому скрипт, перебирающий числа от единицы, выгружает состояние всех заказов магазина за несколько минут — вместе с трек-номерами, по которым на сайте службы доставки виден адрес получателя.
Связь «этот Telegram-аккаунт имеет право на этот заказ» нужно установить явно. Рабочая схема — привязка при оформлении: клиент переходит в бота по ссылке t.me/yourbot?start=<одноразовый_код>, бот получает код в /start, находит по нему пользователя сайта и сохраняет chat_id в его профиль. Дальше проверка сводится к сравнению владельца заказа с сохранённым chat_id.
Более простой, хотя и менее строгий вариант для магазинов без личного кабинета: спрашивать номер заказа вместе с последними четырьмя цифрами телефона и сверять оба значения. Это не криптография, но перебор становится бессмысленным.
Отвечайте Telegram быстро. Если обработчик не уложится в таймаут, Telegram посчитает доставку неудачной и пришлёт то же обновление снова — а ваш бот отправит пользователю второй такой же ответ. Тяжёлую работу выносите за пределы обработки вебхука, а для защиты от повторов храните обработанные update_id: это поле есть в каждом обновлении и служит готовым ключом идемпотентности.
Шаг 5: Уведомление об изменении статуса
Клиенту полезно знать, когда заказ отправлен:
<?php
// local/classes/Events/OrderHandler.php (дополнение)
EventManager::getInstance()->addEventHandler(
'sale',
'OnSaleStatusOrder',
['Local\\Events\\OrderHandler', 'onStatusChange']
);
// В классе OrderHandler:
public static function onStatusChange(
int $orderId,
string $newStatus
): void {
// Отправляем только при переходе в "Отправлен"
if ($newStatus !== 'F') { // F = shipped в стандартной конфигурации
return;
}
$order = Order::load($orderId);
if (!$order) return;
// Ищем телефон/telegram клиента
$props = $order->getPropertyCollection();
$telegram = null;
foreach ($props as $prop) {
if ($prop->getField('CODE') === 'TELEGRAM_ID') {
$telegram = $prop->getValue();
break;
}
}
if (!$telegram) return;
// Получаем трек-номер
$trackNumber = '';
foreach ($order->getShipmentCollection() as $shipment) {
if (!$shipment->isSystem()) {
$trackNumber = $shipment->getField('TRACKING_NUMBER');
break;
}
}
$bot = new Bot(TELEGRAM_BOT_TOKEN);
$message = "🚚 Ваш заказ #{$orderId} отправлен!";
if ($trackNumber) {
$message .= "\n\nТрек-номер: <code>{$trackNumber}</code>";
$message .= "\n\n<a href=\"https://www.pochta.ru/tracking#{$trackNumber}\">Отследить</a>";
}
$bot->sendMessage($telegram, $message);
}Отправка уведомлений: не в обработчике события
Отдельно про то, где вызывать sendMessage() при создании заказа.
Соблазн понятен: повесить обработчик на событие сохранения заказа и отправить сообщение прямо оттуда. Так делают почти все, и это работает — пока Telegram доступен. В момент, когда API отвечает медленно или лежит, оформление заказа у покупателя начинает висеть эти самые секунды таймаута, а при неудачном стечении — падает с ошибкой. Магазин перестаёт принимать заказы из-за недоступности мессенджера, что абсурдно.
Правильная схема: обработчик события только ставит задачу (запись в таблицу очереди или в свою сущность), а отправкой занимается отдельный процесс — агент через cron или воркер. Тогда недоступность Telegram означает задержку уведомления, а не отказ в оформлении.
Побочная выгода: появляется место для повторов. Telegram вернул ошибку — задача осталась в очереди и уйдёт через минуту, а не потеряется навсегда.
Итоги
Код отправки сообщений в Telegram простой, и написать его — самая лёгкая часть задачи. Что действительно требует внимания:
Токен и секрет вебхука — вне репозитория. Токен даёт полный контроль над ботом.
Вебхук проверяет secret_token. Без этого адрес скрипта — единственная преграда, а он не секрет.
Данные о заказе выдаются только владельцу. Номера заказов последовательны; выдача по номеру без проверки — это выгрузка базы заказов для любого желающего.
Отправка уведомлений — асинхронная. Внешний сервис не должен влиять на возможность оформить заказ.
Обработка входящих идемпотентна по update_id — Telegram повторяет доставку.
Что стоит добавить дальше: inline-кнопки для действий менеджера прямо из уведомления (подтвердить, отменить), привязку чата к пользователю сайта при оформлении и уведомления об изменении статуса — но только после того, как закрыты пункты выше.
Частые вопросы
Как защитить Telegram-вебхук от подделки?
Передать параметр secret_token при регистрации вебхука через setWebhook и сверять его первой строкой обработчика. Telegram присылает это значение в заголовке X-Telegram-Bot-Api-Secret-Token с каждым обновлением; сравнивать нужно через hash_equals, при несовпадении отдавать 403. Без этой проверки адрес скрипта — единственная преграда, а он не секрет: попадает в логи, в историю команд и подбирается по типовым путям. Кто угодно сможет отправить туда поддельное обновление с любым chat_id и любым текстом.
Почему нельзя отдавать статус заказа по одному номеру?
Потому что номера заказов последовательны, и скрипт, перебирающий числа от единицы, выгрузит состояние всех заказов магазина за несколько минут — вместе с трек-номерами, по которым на сайте службы доставки виден адрес получателя. Связь «этот Telegram-аккаунт имеет право на этот заказ» нужно устанавливать явно, а не полагаться на знание номера.
Как привязать Telegram-аккаунт к пользователю сайта?
Через одноразовый код при оформлении: клиент переходит в бота по ссылке вида t.me/yourbot?start=КОД, бот получает код в команде /start, находит по нему пользователя сайта и сохраняет chat_id в его профиль. Дальше проверка сводится к сравнению владельца заказа с сохранённым chat_id. Более простой, хотя и менее строгий вариант для магазинов без личного кабинета: спрашивать номер заказа вместе с последними четырьмя цифрами телефона и сверять оба значения — это не криптография, но перебор становится бессмысленным.
Где хранить токен Telegram-бота?
В конфигурации вне репозитория — через Configuration::getValue() или переменную окружения, а не в константе init.php, которая уедет в git. Токен — это полноценный доступ к боту: кто им владеет, тот может читать все входящие сообщения и писать от имени бота всем, кто с ним общался. Если токен всё-таки засветился, отзывайте его командой /revoke у BotFather — смена пароля тут не поможет.
Почему уведомления нельзя отправлять прямо из обработчика события?
Потому что в момент, когда API Telegram отвечает медленно или лежит, оформление заказа у покупателя начинает висеть на таймауте, а при неудачном стечении падает с ошибкой — магазин перестаёт принимать заказы из-за недоступности мессенджера, что абсурдно. Правильная схема: обработчик события только ставит задачу в очередь, а отправкой занимается агент через cron или воркер. Побочная выгода — появляется место для повторов: Telegram вернул ошибку, задача осталась в очереди и уйдёт через минуту, а не потеряется навсегда.
Почему бот отправляет один и тот же ответ дважды?
Потому что обработчик не уложился в таймаут, Telegram посчитал доставку неудачной и прислал то же обновление снова. Тяжёлую работу выносите за пределы обработки вебхука, а для защиты от повторов храните обработанные update_id — это поле есть в каждом обновлении и служит готовым ключом идемпотентности.