Under high load or unstable performance of external services, synchronous processing of incoming requests from Telegram Bot API inevitably leads to failures. If your server does not respond with an HTTP 200 OK code within a few seconds, Telegram will start resending the exact same events (Update). This creates a snowballing load spike and duplicate actions in your system.
In this guide, we will walk through creating an architecturally robust controller in Yii2 that instantly accepts a Webhook, verifies header authenticity, blocks duplicate requests by update_id, and offloads task processing to a background environment via the yii2-queue component.
1. Why it is important to respond with 200 OK instantly
The Telegram Bot API expects a fast response with a 200 OK code from the webhook. If the response time exceeds the allowed timeout, the Telegram server considers delivery to have failed. As a result of retries, your bot might record the same customer in the database multiple times or send them duplicate notifications.
To avoid this, the controller should perform minimal work: verify the секретный токен, record the receipt of the update_id, put the raw array data into a queue, and immediately return a successful acceptance status.
2. Disabling CSRF and validating X-Telegram-Bot-Api-Secret-Token
By default, yii\web\Controller checks the CSRF token for all POST requests. Telegram knows nothing about Yii2 CSRF cookies, so validation must be disabled for the webhook action. To protect against forged requests, the secret_token mechanism is used, which is passed when configuring setWebhook.
The Telegram server sends this secret in the X-Telegram-Bot-Api-Secret-Token header. String comparison must be performed using the timing-safe function hash_equals() to eliminate timing attacks.
3. WebhookController Controller for Yii2
Below is the complete controller code. We disable CSRF, validate the header in the beforeAction method, verify the uniqueness of update_id in the database, and push the task to the queue.
<?php
namespace app\controllers;
use Yii;
use yii\web\Controller;
use yii\web\BadRequestHttpException;
use yii\web\Response;
use app\jobs\ProcessTelegramUpdateJob;
class WebhookController extends Controller
{
public $enableCsrfValidation = false;
public function beforeAction($action)
{
if ($action->id === 'index') {
$secret = getenv('TELEGRAM_SECRET_TOKEN') ?: (Yii::$app->params['telegram_secret_token'] ?? null);
$headerToken = Yii::$app->request->headers->get('X-Telegram-Bot-Api-Secret-Token');
if (!$secret || !$headerToken || !hash_equals($secret, (string)$headerToken)) {
Yii::warning('Невалидный X-Telegram-Bot-Api-Secret-Token', 'telegram');
throw new BadRequestHttpException('Access denied');
}
}
return parent::beforeAction($action);
}
public function actionIndex()
{
Yii::$app->response->format = Response::FORMAT_JSON;
$rawBody = Yii::$app->request->getRawBody();
$update = json_decode($rawBody, true);
if (!$update || !isset($update['update_id'])) {
return ['status' => 'error', 'message' => 'Invalid payload'];
}
$updateId = (int)$update['update_id'];
$db = Yii::$app->db;
// Проверка идемпотентности через атомарную вставку или выборку
$exists = $db->createCommand('SELECT 1 FROM {{%telegram_updates}} WHERE update_id = :id', [
':id' => $updateId,
])->queryScalar();
if ($exists) {
return ['status' => 'ok', 'message' => 'Duplicate update'];
}
$db->createCommand()->insert('{{%telegram_updates}}', [
'update_id' => $updateId,
'created_at' => date('Y-m-d H:i:s'),
])->execute();
// Отправка в Yii Queue
Yii::$app->queue->push(new ProcessTelegramUpdateJob([
'update' => $update,
]));
return ['status' => 'ok'];
}
}
4. Background update processing via Yii Queue Job
The queue job extracts the update body, parses the incoming text or commands, executes business logic (such as lead generation), and formats a response back to the user. Below is an example of the ProcessTelegramUpdateJob class.
<?php
namespace app\jobs;
use Yii;
use yii\base\BaseObject;
use yii\queue\JobInterface;
use app\components\TelegramSender;
class ProcessTelegramUpdateJob extends BaseObject implements JobInterface
{
public array $update = [];
public function execute($queue)
{
if (!isset($this->update['message'])) {
return;
}
$message = $this->update['message'];
$chatId = $message['chat']['id'] ?? null;
$text = trim($message['text'] ?? '');
if (!$chatId) {
return;
}
if (str_starts_with($text, '/start')) {
$leadId = bin2hex(random_bytes(7));
Yii::$app->db->createCommand()->insert('{{%leads}}', [
'lead_id' => $leadId,
'chat_id' => $chatId,
'status' => 'new',
'created_at' => date('Y-m-d H:i:s'),
])->execute();
$sender = new TelegramSender();
$sender->sendMessage([
'chat_id' => $chatId,
'text' => "Заявка принята! Ваш номер лида: <strong>{$leadId}</strong>",
'parse_mode' => 'HTML',
]);
}
}
}
5. Message sending service via cURL in Telegram API
To interact with the Telegram API, it is important to use a clean cURL connection with checks for the HTTP response code, JSON decoding errors, and the ok: true structure.
<?php
namespace app\components;
use Yii;
use RuntimeException;
class TelegramSender
{
private string $token;
public function __construct()
{
$this->token = (string)(getenv('TELEGRAM_BOT_TOKEN') ?: (Yii::$app->params['telegram_bot_token'] ?? ''));
if (empty($this->token)) {
throw new RuntimeException('Не задан TELEGRAM_BOT_TOKEN');
}
}
public function sendMessage(array $params): array
{
return $this->request('sendMessage', $params);
}
public function request(string $method, array $params = []): array
{
$url = 'https://api.telegram.org/bot' . $this->token . '/' . $method;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($params),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
]);
$response = curl_exec($ch);
$curlError = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false) {
throw new RuntimeException('Ошибка cURL при запросе к Telegram: ' . $curlError);
}
$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException('Невалидный JSON ответа от Telegram API');
}
if ($httpCode !== 200 || !isset($data['ok']) || $data['ok'] !== true) {
$description = $data['description'] ?? 'Неизвестная ошибка API';
Yii::error("Telegram API Error [{$httpCode}]: {$description}", 'telegram');
throw new RuntimeException("Telegram API Error: {$description}");
}
return $data['result'];
}
}
6. Common mistakes when implementing Webhooks in Yii2
- Forgetting return true in beforeAction: If you do not return the result of
parent::beforeAction($action), controller execution will stop and the action will not be called. - Hardcoding tokens: The bot token and secret key should be passed via переменные окружения (
getenv()) or an isolated configuration fileparams-local.php. - Synchronous external requests: Calling third-party CRMs or AI services directly inside the controller body inevitably causes timeouts and forces Telegram to resend duplicate messages.
The BotCreator team will develop a fault-tolerant Telegram-бот of any complexity for your business with full integration into the company's internal systems.