Для продакшн-бота webhook — основний робочий режим. Long polling зручно для локальної відладки, але на боєвої сервері створює надлишкову навантаження і гірше масштабується.
\nTelegram вимагає HTTPS з валидним сертифікатом, порт 443 / 80 / 88 / 8443 і відповідь 200 OK за кілька секунд. Нижче — мінімальний стек на чистим PHP 8.1+ без фреймворка: реєстрація webhook, перевірка секрету, ідемпотентність за update_id та швидка відповідь.
Требования до сервера та PHP
\nВимагаються розширення curl, json, mbstring, openssl та pdo. Перевірка:
php -m | grep -E 'curl|json|pdo|mbstring|openssl'\nВеб-сервер має віддавати скрипт за публічним HTTPS-URL, наприклад https://bot.example.com/webhook.ph p. Переконайтеся, що post_max_size та upload_max_filesize не обрізають тело запиту (при файлах обновлення може бути більшим за звичайний JSON).
Реєстрація webhook: setWebhook
\nНе викликайте 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";\nСекрет можна згенерувати так:
\nphp -r 'echo bin2hex(random_bytes(32)), PHP_EOL;'\n\nТочка входу: webhook.php
\nСкрипт має працювати швидко: прочитати тело, перевірити секрет через hash_equals, розпарсувати JSON, закреслити update_id, додати завдання до черги та повернути 200. Важку логіку (рассити, зовнішні API, записування lead'ів) виконуйте в воркері, а не в 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';\n\n\n\nВажливо: флаг
\nJSON_BIGINT_AS_STRINGзберігаєupdate_id,user.idтаchat.idяк рядки. Така підхід дозволяє не втрачати точність у 32-бітних збірках PHP та при великих chat id.
Схема БД для ідемпотентності
\nTelegram може надіслати один і той самий 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;\nСтарі записи в processed_updates можна очищувати за допомогою cron — Telegram гарантує монотонний зростання update_id, дуже старі id вже не повертаються:
DELETE FROM processed_updates
WHERE received_at < NOW() - INTERVAL 14 DAY;\n\nОбработка апдейтів у воркері
\nВоркер забирає завдання зі 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));
}
}\n\nПроверка
\nПісля деплою викличте getWebhookInfo: мають бути ваш URL, відсутність помилок доставки та очікуваний список allowed_updates.
curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getWebhookInf o" | jq .\nСвязанні матеріали: long polling для локальной разработки, webhook-контроллер на Yii2, глава гайду Webhook для Telegram-бота.
"}