Для продакшн-ботов webhook — основной рабочий режим. Long polling удобен для локальной отладки, но на боевом сервере даёт лишнюю нагрузку и хуже масштабируется.
Telegram требует HTTPS с валидным сертификатом, порт 443 / 80 / 88 / 8443 и ответ 200 OK за несколько секунд. Ниже — минимальный стек на чистом PHP 8.1+ без фреймворка: регистрация webhook, проверка секрета, идемпотентность по update_id и быстрый ответ.
Требования к серверу и PHP
Нужны расширения curl, json, mbstring, openssl и pdo. Проверка:
php -m | grep -E 'curl|json|pdo|mbstring|openssl'
Веб-сервер должен отдавать скрипт по публичному HTTPS-URL, например https://bot.example.com/webhook.ph p. Убедитесь, что post_max_size и upload_max_filesize не режут тело запроса (при файлах апдейт может быть крупнее обычного JSON).
Регистрация webhook: setWebhook
Не вызывайте Bot API через file_get_contents — нет нормального контроля HTTP-кодов и таймаутов. Однократный CLI-скрипт удобен при деплое и ротации токена.
Параметр secret_token сгенерируйте один раз и храните в окружении. Telegram будет присылать его в заголовке X-Telegram-Bot-Api-Secret-Token — это защита от чужих POST на ваш endpoint. drop_pending_updates чистит очередь при перерегистрации; max_connections ограничивает параллельную доставку (1–100).
<?php
// set_webhook.php — запускать из CLI
declare(strict_types=1);
$token = getenv('TELEGRAM_BOT_TOKEN') ?: exit("TELEGRAM_BOT_TOKEN missing\n");
$secret = getenv('TELEGRAM_WEBHOOK_SECRET') ?: exit("TELEGRAM_WEBHOOK_SECRET missing\n");
$url = 'https://bot.example.com/webhook.ph p';
$ch = curl_init('https://api.telegram.org/bo t' . $token . '/setWebhook');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'url' => $url,
'secret_token' => $secret,
'max_connections' => 40,
'drop_pending_updates' => true,
'allowed_updates' => ['message', 'callback_query'],
], JSON_UNESCAPED_UNICODE),
CURLOPT_TIMEOUT => 20,
]);
$response = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP {$code}\n{$response}\n";
Секрет можно сгенерировать так:
php -r 'echo bin2hex(random_bytes(32)), PHP_EOL;'
Точка входа: webhook.php
Скрипт должен отработать быстро: прочитать тело, проверить секрет через hash_equals, распарсить JSON, зафиксировать update_id, поставить задачу в очередь и вернуть 200. Тяжёлую логику (рассылки, внешние API, записи лидов) выполняйте в воркере, а не в HTTP-запросе.
<?php
// webhook.php
declare(strict_types=1);
$secret = getenv('TELEGRAM_WEBHOOK_SECRET') ?: '';
$header = $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? '';
if ($secret === '' || !hash_equals($secret, $header)) {
http_response_code(401);
exit;
}
$raw = file_get_contents('php://input');
$update = json_decode((string) $raw, true, 512, JSON_BIGINT_AS_STRING);
if (!is_array($update) || !isset($update['update_id'])) {
http_response_code(400);
exit;
}
$updateId = (string) $update['update_id'];
// PDO: INSERT IGNORE / ON CONFLICT — дубликат update_id = уже обработан
$pdo = new PDO(getenv('DSN'), getenv('DB_USER'), getenv('DB_PASS'), [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$stmt = $pdo->prepare(
'INSERT IGNORE INTO processed_updates (update_id, received_at) VALUES (?, NOW())'
);
$stmt->execute([$updateId]);
if ($stmt->rowCount() === 0) {
// повторная доставка того же update_id
http_response_code(200);
echo 'ok';
exit;
}
$job = $pdo->prepare(
'INSERT INTO jobs (payload, status, created_at) VALUES (?, 0, NOW())'
);
$job->execute([$raw]);
http_response_code(200);
echo 'ok';
Важно: флаг
JSON_BIGINT_AS_STRINGсохраняетupdate_id,user.idиchat.idкак строки. Так вы не теряете точность на 32-битных сборках PHP и при больших chat id.
Схема БД для идемпотентности
Telegram может прислать один и тот же Update повторно. Храните обработанные update_id с уникальным ключом и пропускайте дубли. Таблица должна быть в той же транзакционной БД, что и бизнес-данные.
CREATE TABLE processed_updates (
update_id VARCHAR(32) NOT NULL,
received_at DATETIME NOT NULL,
PRIMARY KEY (update_id)
) ENGINE=InnoDB;
CREATE TABLE jobs (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
payload MEDIUMTEXT NOT NULL,
status TINYINT NOT NULL DEFAULT 0, -- 0 new, 1 running, 2 done, 3 failed
attempts INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
updated_at DATETIME NULL,
PRIMARY KEY (id),
KEY idx_jobs_status (status, id)
) ENGINE=InnoDB;
Старые записи в processed_updates можно чистить по крону — Telegram гарантирует монотонный рост update_id, совсем древние id уже не вернутся:
DELETE FROM processed_updates
WHERE received_at < NOW() - INTERVAL 14 DAY;
Обработка апдейтов в воркере
Воркер забирает задачи со status = 0, помечает status = 1, парсит JSON и выполняет логику бота. При успехе — status = 2. При ошибке увеличивайте attempts, делайте backoff; после лимита — status = 3 и алерт. Так HTTP-webhook всегда отвечает 200 за доли секунды, а бизнес-логика не ломается из-за таймаута Telegram.
<?php
// worker.php — крутить через supervisor / systemd
declare(strict_types=1);
$pdo = new PDO(getenv('DSN'), getenv('DB_USER'), getenv('DB_PASS'), [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
while (true) {
$pdo->beginTransaction();
$row = $pdo->query(
'SELECT id, payload, attempts FROM jobs WHERE status = 0 ORDER BY id ASC LIMIT 1 FOR UPDATE'
)->fetch(PDO::FETCH_ASSOC);
if (!$row) {
$pdo->commit();
usleep(300000);
continue;
}
$pdo->prepare('UPDATE jobs SET status = 1, updated_at = NOW() WHERE id = ?')
->execute([$row['id']]);
$pdo->commit();
try {
$update = json_decode($row['payload'], true, 512, JSON_BIGINT_AS_STRING);
// handleUpdate($update); — ваша логика бота
$pdo->prepare('UPDATE jobs SET status = 2, updated_at = NOW() WHERE id = ?')
->execute([$row['id']]);
} catch (Throwable $e) {
$attempts = (int) $row['attempts'] + 1;
$status = $attempts >= 5 ? 3 : 0;
$pdo->prepare(
'UPDATE jobs SET status = ?, attempts = ?, updated_at = NOW() WHERE id = ?'
)->execute([$status, $attempts, $row['id']]);
error_log($e->getMessage());
sleep(min(30, $attempts * 2));
}
}
Проверка
После деплоя вызовите getWebhookInfo: должны быть ваш URL, отсутствие ошибок доставки и ожидаемый список allowed_updates.
curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getWebhookInf o" | jq .
Связанные материалы: long polling для локальной разработки, webhook-контроллер на Yii2, глава гайда Webhook для Telegram-бота.