Обновления
Webhook
Практическое руководство по настройке webhook для Telegram-бота на PHP: установка через setWebhook, проверка getWebhookInfo, приём обновлений, идемпотентность и чек-лист для production.
В режиме webhook Telegram сам отправляет каждый Update POST-запросом на ваш публичный HTTPS-эндпоинт. Это единственный вариант для production: бот не держит постоянное соединение, а реагирует на входящие события. Главное — ответить 200 OK за 1–2 секунды, иначе Telegram будет повторять доставку с экспоненциальной задержкой (до нескольких минут).
Что нужно перед стартом
- Публичный домен с валидным TLS-сертификатом (Let's Encrypt подходит, самоподписанные — нет).
- PHP-скрипт, доступный по HTTPS (например,
https://example.com/telegram/webhook). - Секретный токен для проверки подлинности запросов (генерируем один раз, храним в
.env). - На сервере должен быть доступен
php://input(в nginx + php-fpm работает из коробки).
Установка webhook через setWebhook
Используем функцию telegramApi(), которую мы сделали в главе Первый запрос: getMe. Там мы написали обёртку над cURL с проверкой HTTP-кода, json_last_error и поля ok — здесь просто вызываем её.
<?php
// set_webhook.php
require_once __DIR__ . '/src/TelegramClient.php'; // telegramApi()
$token = getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
die('TELEGRAM_BOT_TOKEN не задан в окружении\n');
}
// Секрет 1–256 символов: A-Z a-z 0-9 _ -
$secret = getenv('TELEGRAM_WEBHOOK_SECRET') ?: bin2hex(random_bytes(16));
$webhookUrl = 'https://example.com/telegram/webhook';
$resp = telegramApi($token, 'setWebhook', [
'url' => $webhookUrl,
'secret_token' => $secret,
'allowed_updates' => ['message', 'callback_query', 'inline_query'],
'drop_pending_updates' => true, // чистим хвост от polling
]);
// Разбор ответа
if (!($resp['ok'] ?? false)) {
echo "Ошибка setWebhook: " . json_encode($resp, JSON_UNESCAPED_UNICODE) . "\n";
exit(1);
}
echo "Webhook установлен: " . $webhookUrl . "\n";
echo "Secret (сохраните в .env): " . $secret . "\n";
// Пример ответа:
// {"ok":true,"result":true,"description":"Webhook was set"}
Пример JSON-ответа API:
{
"ok": true,
"result": true,
"description": "Webhook was set"
}
Поле ok — главный индикатор успеха. result: true подтверждает, что URL принят. Если ok: false — смотрите description (часто: "Bad webhook: wrong certificate" или "Bad webhook: wrong URL"). В терминале запустите php set_webhook.php — увидите URL и сгенерированный секрет. Сохраните секрет в .env как TELEGRAM_WEBHOOK_SECRET=....
Проверка текущего webhook — getWebhookInfo
Полезно после деплоя или при отладке повторных доставок. Метод не требует параметров.
<?php
// get_webhook_info.php
require_once __DIR__ . '/src/TelegramClient.php';
$token = getenv('TELEGRAM_BOT_TOKEN');
$info = telegramApi($token, 'getWebhookInfo');
echo json_encode($info, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
Пример ответа:
{
"ok": true,
"result": {
"url": "https://example.com/telegram/webhook",
"has_custom_certificate": false,
"pending_update_count": 0,
"last_error_date": 1700000000,
"last_error_message": "Wrong response from the webhook: 500 Internal Server Error",
"max_connections": 40,
"allowed_updates": ["message", "callback_query"]
}
}
Смотрите pending_update_count (сколько апдейтов в очереди), last_error_message (последняя ошибка доставки) и last_error_date (Unix-время). Если ошибок нет — поля last_error_* отсутствуют. Запустите php get_webhook_info.php и проверьте: pending_update_count: 0 и нет last_error_message — значит всё чисто.
Приём обновлений: эндпоинт webhook
Скрипт должен: проверить секрет, распарсить JSON, отдать 200 OK мгновенно, тяжёлую работу — в очередь (БД, Redis, RabbitMQ). Ниже — минимальный рабочий пример без очереди (для понимания).
<?php
// public/telegram/webhook.php
require_once __DIR__ . '/../../src/TelegramClient.php';
$secret = getenv('TELEGRAM_WEBHOOK_SECRET');
if (!$secret) {
error_log('TELEGRAM_WEBHOOK_SECRET не задан');
http_response_code(500);
exit;
}
// 1. Проверка секрета (заголовок X-Telegram-Bot-Api-Secret-Token)
$header = $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? '';
if (!hash_equals($secret, $header)) {
error_log('Invalid secret token');
http_response_code(401);
exit;
}
// 2. Чтение raw body
$raw = file_get_contents('php://input');
if ($raw === false) {
http_response_code(400);
exit;
}
// 3. Парсинг JSON
$update = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($update)) {
error_log('Invalid JSON: ' . json_last_error_msg());
http_response_code(400);
exit;
}
// 4. Логируем для отладки (в production — только update_id)
error_log('Update received: ' . json_encode([
'update_id' => $update['update_id'] ?? null,
'type' => array_keys(array_filter($update, 'is_array', ARRAY_FILTER_USE_KEY))[0] ?? 'unknown'
]));
// 5. Идемпотентность: сохраняем update_id в БД с UNIQUE
// $pdo->prepare('INSERT IGNORE INTO processed_updates (update_id) VALUES (?)')->execute([$update['update_id']]);
// if ($pdo->lastInsertId() === 0) { http_response_code(200); exit; } // дубликат
// 6. Быстрый ACK — Telegram ждёт 200
http_response_code(200);
echo 'ok';
// 7. Дальше — асинхронная обработка (Job/Worker)
// processUpdate($update); // функция из главы Long polling
Как посмотреть, что пришло: в терминале tail -f /var/log/nginx/error.log (или ваш php-fpm лог) — увидите строки Update received: {...}. Для детального инспекта добавьте временно:
error_log('FULL UPDATE: ' . json_encode($update, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
После этого в логе будет полный JSON с полями message, callback_query и т.д. — удобно для понимания структуры при разработке.
Повторные доставки и идемпотентность
Telegram может прислать один Update несколько раз (сетевой сбой, таймаут вашего скрипта, редеплой). Храните обработанные update_id в таблице с UNIQUE-индексом и пропускайте дубликаты на входе. Один in-memory курсор не спасает при нескольких инстансах (php-fpm workers, Kubernetes pods). Пример схемы таблицы:
CREATE TABLE processed_updates (
update_id BIGINT PRIMARY KEY,
received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
В коде (раскомментируйте блок 5 в webhook.php) — INSERT IGNORE молча пропустит дубликат, lastInsertId() === 0 покажет, что апдейт уже был.
Удаление webhook (вернуться к polling)
<?php
// delete_webhook.php
require_once __DIR__ . '/src/TelegramClient.php';
$token = getenv('TELEGRAM_BOT_TOKEN');
$resp = telegramApi($token, 'deleteWebhook', [
'drop_pending_updates' => true, // сбросить накопившуюся очередь
]);
var_dump($resp);
После deleteWebhook можно снова использовать getUpdates с long polling. Ответ будет {"ok":true,"result":true,"description":"Webhook deleted"}.
Чек-лист перед production
setWebhookвернулok: true(см. выводphp set_webhook.phpвыше).getWebhookInfo:pending_update_count: 0и нетlast_error_message.- Эндпоинт отвечает
200 OKбыстрее чем за 1 секунду на тестовомUpdate(проверка черезcurl— ниже). - Секрет уходит в заголовке
X-Telegram-Bot-Api-Secret-Token, не в query string — иначе попадёт в access-логи nginx. - Идемпотентность: таблица
processed_updatesс UNIQUE поupdate_id. - Тяжёлая логика (HTTP наружу, БД, CRM) — в воркере; webhook только принимает апдейт и отвечает
ok.
Как прогнать тестовый Update через curl
Это имитация запроса от Telegram: вы сами шлёте JSON на свой HTTPS-эндпоинт и смотрите код ответа.
Нужны два значения, которые уже появились выше в этой главе:
- URL webhook — тот же адрес, что передавали в
setWebhook(раздел Установка webhook через setWebhook). - Секрет — строка
secret_token/ переменнаяTELEGRAM_WEBHOOK_SECRET. Её мы сгенерировали и сохранили в.envв том же разделе Установка webhook; Telegram потом присылает её в заголовкеX-Telegram-Bot-Api-Secret-Token. Без этого заголовка ваш скрипт ответит401.
Токен бота (TELEGRAM_BOT_TOKEN) для этого теста curl не нужен — вы бьёте в свой сервер, не в api.telegram.org. Токен нужен только когда вызываете Bot API через telegramApi() (глава Первый запрос: getMe).
Сохраните тело апдейта в файл (так удобнее, чем одна гигантская строка в терминале):
cat > /tmp/tg-update.json <<'EOF'
{
"update_id": 1,
"message": {
"message_id": 1,
"date": 1700000000,
"chat": { "id": 123, "type": "private" },
"from": { "id": 123, "is_bot": false, "first_name": "Test" },
"text": "/start"
}
}
EOF
Отправьте POST. В $secret подставьте тот же TELEGRAM_WEBHOOK_SECRET, что сохранили после setWebhook (из .env или вывод php set_webhook.php):
export secret="$TELEGRAM_WEBHOOK_SECRET"
# или: export secret='ваш_секрет_из_env'
curl -sS -o /tmp/tg-webhook-body.txt -w "HTTP %{http_code} time %{time_total}s\n" \
-X POST "https://example.com/telegram/webhook" \
-H "Content-Type: application/json" \
-H "X-Telegram-Bot-Api-Secret-Token: $secret" \
--data-binary @/tmp/tg-update.json
Ожидаемый результат в терминале: строка вида HTTP 200 time 0.05s (время заметно меньше 1 секунды). Тело ответа обычно ok — его можно посмотреть так:
cat /tmp/tg-webhook-body.txt
Если получили 401 — не совпал секрет. 400 — битый JSON. 500 — ошибка в PHP (смотрите error.log). Пока не будет стабильного 200, Telegram в production будет копить pending_update_count и писать last_error_message в getWebhookInfo.
Связанные главы: getUpdates и update_id, Long polling, HTTP-клиент на PHP, проект Заявка с сайта → Telegram.
Дальше: С polling на webhook — как корректно мигрировать работающего бота без потери обновлений.