Перейти к содержанию
9 минут чтения
Тема: Интеграции Telegram Bot API · Темы групп · PHP

Как отправить сообщение в тему Telegram-группы: chat_id и message_thread_id

В группе с темами уведомления удобно разделять: заказы в одном разделе, оплаты в другом, технические ошибки в третьем. Для примера настроим отправку тестового уведомления в тему «Заказы»: найдём её идентификатор, выполним один запрос на PHP и проверим адрес доставки.

Пример ниже действительно отправляет сообщение. Используйте отдельную тестовую группу и своего бота. Токен и API-ключ не нужно публиковать, вставлять в скриншоты или сохранять вместе с кодом.

Что подготовить перед отправкой

Здесь рассматриваем обычную супергруппу с включёнными темами и уже созданную тему «Заказы». Личные диалоги с темами, комментарии к публикациям канала и личные сообщения каналу не входят в этот сценарий.

  • Добавьте своего бота в тестовую группу и разрешите ему отправлять текстовые сообщения.

  • Откройте нужную тему и убедитесь, что она доступна для отправки. На время теста не удаляйте и не пересоздавайте её.

  • Подготовьте доступ к входящим обновлениям этого бота через уже работающий обработчик.

  • Для PHP-примера нужен 64-битный PHP 8.2+ с расширением cURL. Запускайте файл из терминала, не через публичный URL.

Если бот ещё не создан, начните с инструкции по BotFather. Для уже работающего бота не меняйте webhook только ради получения номера темы.

Не путайте группу, тему и отдельное сообщение

Поле

Что выбирает

В нашем примере

chat_id

Группу, в которую отправляем сообщение

-1001234567890

message_thread_id

Нужную тему внутри этой группы

73

message_id

Отдельное сообщение внутри чата

418

Числа в таблице вымышленные. Сохраните настройки адресата парой: группа и тема. Не подставляйте ID произвольного сообщения вместо ID темы и не переносите номер темы из другой группы.

При отправке sendMessage принимает chat_id и message_thread_id. У входящего сообщения эти значения находятся в message.chat.id и message.message_thread_id. Описание полей есть в Telegram Bot API.

Получите ID темы из нового сообщения

Откройте «Заказы» и от своего пользовательского аккаунта отправьте команду /topic@your_bot, заменив your_bot на username вашего бота. Адресованная команда подходит для проверки даже при включённом Privacy Mode. Боту не обязательно отвечать на неё: сначала нам нужно увидеть само входящее обновление.

В отладке уже проверенного обработчика найдите сообщение именно из этой темы. Ниже сокращённый пример тела обновления: не относящиеся к задаче поля опущены.

JSON · фрагмент входящего обновления
{
  "update_id": 900001,
  "message": {
    "message_id": 418,
    "message_thread_id": 73,
    "chat": {
      "id": -1001234567890,
      "title": "Тестовые уведомления",
      "type": "supergroup",
      "is_forum": true
    },
    "is_topic_message": true,
    "text": "/topic@your_bot"
  }
}

Для этого примера сохраняем chat_id=-1001234567890 и message_thread_id=73. Номер 418 относится только к присланной команде.

  • Сверьте название и ID группы: бот может одновременно состоять в нескольких чатах.

  • Если message_thread_id отсутствует, отправьте новую команду в явно выбранной отдельной теме и снова проверьте обновление. Не подставляйте случайное число.

  • Если обновление вообще не пришло, сначала проверьте получение сообщений, а не исходящий sendMessage.

Privacy Mode описан в документации Telegram. Для диагностики доставки используйте руководство по webhook. Не вызывайте deleteWebhook и не очищайте ожидающие обновления ради этого теста.

Если тему создаёт ваша программа через createForumTopic, сохраните возвращённый message_thread_id вместе с ID группы. Создание темы требует дополнительных прав; для получения номера существующей темы создавать новую не нужно. Документация createForumTopic.

Отправьте одно тестовое сообщение на PHP

Создайте файл send-to-topic.php с кодом ниже. Он читает переменные окружения процесса, отправляет один запрос без автоматических повторов и выводит идентификаторы из ответа. Файл .env самостоятельно не загружается; Composer для этого примера не нужен.

PHP · send-to-topic.php
<?php

declare(strict_types=1);

$requiredEnv = static function (string $name): string {
    $value = getenv($name);
    if ($value === false || trim($value) === '') {
        throw new RuntimeException('Missing environment variable: '.$name);
    }
    return trim($value);
};

try {
    $chatId = $requiredEnv('TELEGRAM_CHAT_ID');
    $threadId = filter_var(
        $requiredEnv('TELEGRAM_THREAD_ID'),
        FILTER_VALIDATE_INT,
        ['options' => ['min_range' => 1]],
    );
    if (!preg_match('/^-[1-9][0-9]*$/', $chatId) || $threadId === false) {
        throw new RuntimeException('Expected a negative group ID and a positive topic ID.');
    }

    $token = $requiredEnv('TELEGRAM_BOT_TOKEN');
    $url = 'https://api.telegram.org/bot'.$token.'/sendMessage';
    $headers = ['Content-Type: application/json'];

    $payload = [
        'chat_id' => $chatId,
        'message_thread_id' => $threadId,
        'text' => 'Проверка: уведомление для темы «Заказы».',
    ];

    $curl = curl_init($url);
    if ($curl === false) {
        throw new RuntimeException('Cannot initialize cURL.');
    }
    try {
        curl_setopt_array($curl, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE),
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_TIMEOUT => 15,
        ]);
        $raw = curl_exec($curl);
        $status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        if ($raw === false || $status >= 500) {
            throw new RuntimeException('Delivery outcome is unknown. Check the topic before retrying.');
        }
    } finally {
        unset($curl);
    }

    try {
        $response = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
    } catch (JsonException) {
        throw new RuntimeException('Unreadable response. Check the topic before retrying.');
    }
    if (!is_array($response) || !array_key_exists('ok', $response)) {
        throw new RuntimeException('Unexpected response. Check the topic before retrying.');
    }
    if ($status !== 200 || $response['ok'] !== true) {
        fwrite(STDERR, 'Request failed: HTTP '.$status.'; '
            .($response['description'] ?? $response['error'] ?? 'no description').PHP_EOL);
        exit(1);
    }

    echo json_encode([
        'chat_id' => $response['result']['chat']['id'] ?? null,
        'message_thread_id' => $response['result']['message_thread_id'] ?? null,
        'message_id' => $response['result']['message_id'] ?? null,
    ], JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT).PHP_EOL;
} catch (Throwable $exception) {
    fwrite(STDERR, $exception->getMessage().PHP_EOL);
    exit(1);
}

В терминале Bash задайте данные тестового адресата. Вместо чисел из примера используйте полученные значения. Ввод токена скрыт; не включайте set -x и не публикуйте окружение процесса.

Bash · запуск с прямым подключением к Telegram
read -r -s -p 'Telegram bot token: ' TELEGRAM_BOT_TOKEN
printf '\n'
export TELEGRAM_BOT_TOKEN
export TELEGRAM_CHAT_ID='-1001234567890'
export TELEGRAM_THREAD_ID='73'
php send-to-topic.php
unset TELEGRAM_BOT_TOKEN

Скрипт отправляет обычный текст без parse_mode: так ошибка форматирования не мешает проверке адреса. После успешного теста можно добавить свой текст и нужную разметку.

При таймауте или непонятном ответе остановитесь и проверьте тему. Отсутствие подтверждения у программы не доказывает, что сообщение не было отправлено. Повторный запуск может создать дубликат.

Проверьте ответ и сообщение в Telegram

При успешном запросе скрипт выводит примерно такой результат. Это пример, а не универсальные значения для вашей группы:

JSON · результат тестовой отправки
{
  "chat_id": -1001234567890,
  "message_thread_id": 73,
  "message_id": 419
}
  • Сравните chat_id и message_thread_id с настройками адресата. Не ограничивайтесь отсутствием ошибки в терминале.

  • Откройте тему «Заказы» в Telegram и найдите тестовое сообщение. Убедитесь, что оно не появилось в соседней теме.

  • Сохраните пару идентификаторов в конфигурации своей интеграции. Для другой темы получите её отдельный ID.

  • Перед подключением реальных событий проверьте, что один тестовый заказ создаёт ровно одно уведомление.

Обработка принятого запроса и просмотр сообщения человеком — разные вещи. Ответ API не подтверждает, что менеджер уже прочитал уведомление. Если сообщения приходят дважды, используйте разбор причин дублей.

Что проверить при ошибке или неправильной теме

  • Сообщение попало не туда. Проверьте фактически отправленный JSON, а не только значение в настройках. Не потерялся ли message_thread_id при сборке запроса? Совпадает ли chat_id с группой, из которой вы получили обновление?

  • Ответ указывает на проблему с темой. Получите пару ID заново из свежего сообщения в нужной теме. Проверьте, что тему не удалили и не создали повторно под тем же названием. Не исправляйте проблему подбором чисел.

  • Нет доступа к группе или отправке. Сверьте бота, членство в группе и ограничения на отправку. Соседние случаи разобраны в статье про chat not found.

  • Тема закрыта. Уточните её состояние у администратора и откройте нужную тему для теста. Не выдавайте боту все административные права как универсальное решение.

  • Таймаут или HTTP 5xx. Результат может быть неизвестен: сначала проверьте чат и журналы. Далее используйте диагностику соединения.

  • Telegram ограничил частоту запросов. Не запускайте скрипт в цикле. Правила ожидания и retry_after разобраны в руководстве по 429.

Для этого примера выбрана отдельная именованная тема, а не «Общая». Не переносите на неё случайно найденные ID и не смешивайте выбор темы с ответом на конкретное сообщение. Сначала добейтесь воспроизводимой отправки в одну тестовую тему, затем расширяйте сценарий.

Как выполнить тот же запрос через BotGate

Если ваш бот уже подключён к BotGate, пара идентификаторов и тело запроса остаются теми же. Меняются адрес API и авторизация. В send-to-topic.php замените три строки настройки $token, $url и $headers следующим блоком:

PHP · замена настроек подключения
$botId = $requiredEnv('BOTGATE_BOT_ID');
$url = 'https://bot-gate.ru/api/v1/bots/'.rawurlencode($botId).'/sendMessage';
$headers = [
    'Content-Type: application/json',
    'Authorization: Bearer '.$requiredEnv('BOTGATE_API_KEY'),
];

Остальную часть файла менять не нужно. BOTGATE_BOT_ID — публичный ID подключённого бота из кабинета, не Telegram username. Используйте API-ключ владельца этого бота. В этом варианте TELEGRAM_BOT_TOKEN скрипту не нужен.

Bash · запуск через BotGate
read -r -s -p 'BotGate API key: ' BOTGATE_API_KEY
printf '\n'
export BOTGATE_API_KEY
export BOTGATE_BOT_ID='bot_REPLACE_ME'
export TELEGRAM_CHAT_ID='-1001234567890'
export TELEGRAM_THREAD_ID='73'
php send-to-topic.php
unset BOTGATE_API_KEY

Сначала замените bot_REPLACE_ME и тестовые числа своими значениями. Выполните один запуск и повторите проверку ответа и сообщения в выбранной теме.

При получении ID темы используйте обновление из вашего обработчика webhook BotGate после проверки подлинности. Метод getUpdates через сервис недоступен. Формат запроса и проверка webhook описаны в документации BotGate.

BotGate передаёт параметры запроса, но не исправляет неверный номер темы, закрытую тему или отсутствие прав. При ошибке смотрите не только HTTP-статус, но и тело ответа: отказ Telegram проходит через BotGate с HTTP 400 и полями Telegram; собственные ошибки сервиса проверяются отдельно.

Первичные источники