Переклад цього гайду зʼявиться незабаром. Поки що текст доступний російською — перемкніться на RU або читайте нижче. Відкрити російську версію

Оновлення

Webhook

В режиме 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 — как корректно мигрировать работающего бота без потери обновлений.