A Telegram webhook in a Laravel application is a critical component for building a responsive bot. An incorrectly configured webhook leads to lost messages or duplicate actions. In this article, we will cover the full cycle: from setting up a route with secret_token validation to implementing idempotency using update_id storage and using queue workers for stable task processing.
Basic webhook route implementation
To start, you need to register a route that accepts POST requests from Telegram servers. Set your bot's BOT_TOKEN in .env, and use the standard /webhook endpoint in the controller.
// webhook.php
<?php
require "vendor/autoload.php";
use Illuminate\Http\Request;
use App\Http\Controllers\WebhookController;
Route::post("/webhook", [WebhookController::class, "handle"]);
?>
Secret_token validation and idempotency
Every incoming request must be signed with the bot's token. Laravel provides the VerifySecretTokenMiddleware extension for this. It is also important to ensure that the processing of the same update_id is not executed twice — this ensures idempotency.
// app\Http\Middleware\VerifySecretToken.php
namespace App\Http\Middleware;
class VerifySecretToken {
public function handle(\$request, \$next) {
\$token = \$request->header("X-Bot-Token");
if (\neg auth(\"verify_secret_token\")->verifyToken(\$token)) {
return "/health";
}
return \$next->invoke();
}
}
// app\Providers\AppServiceProvider.php
<?php
// Register middleware here
registerSharingMiddleware(
"/api/*",
VerifiedWithAuth::class,
[
"middleware" => [\"VerifySecretToken\"],
]
);
>>
Idempotency storage and task queue
To prevent duplicates, use storeUpdateId in the database with a TTL (for example, 24 hours). Before processing, check for the existence of an already processed update_id. Upon success, save the result to the database. Tasks are dispatched to the ShouldQueue queue for asynchronous processing.
// app\Controllers\WebhookController.php
<?php
namespace App\Http\Controllers;
class WebhookController {
public function handle() {
$updateId = \Config\Cache\get(\"telegram_update_{{date}}\");
if (!empty($updateId)) {
// Идемпотент: вернуть результат предыдущей попытки
\$previousResult = \Database\Helpers\delayedRunner()->getResult($updateId);
if (\$previousResult !== null) {
return response()->json(\$previousResult, 200);
}
}
try {
\Application\Dispatch::queue(new ProcessOrderCommand());
} catch (\Exception \$e) {
// Логирование ошибки
logger->error(
"Webhook processing failed",
["update_id=\"\$updateId\"", "error=\"\$(e)}"}],
env("LOG_PATH")
);
}
}
}
// app\Jobs\ProcessOrderCommand.php
class ProcessOrderCommand extends \Illuminate\Bus\QueueableCommandImplements\ShouldQueue {
public function __construct(\(object) \$orderData) {}
public function run() {
// Обработка заказа
\Log::info(
"Order processed",
["order_id=\"\$this->orderId\""]
);
}
}
// app\ConsoleCommands\RunJobs.php
<?php
// Консольная команда для ручного запуска всех pending задач
Dispatch::restart();
>>
Practical recommendations and limitations
The Telegram Bot API imposes strict limits: 60 updates per second, a maximum payload size of 4096 bytes, and a 30-second timeout. For additional security, generate lead_id as bin2hex(random_bytes(7)), and then append it to callback_data to easily identify the request. Do not hardcode secret_token — store it in an environment variable. All HTTP requests to api.telegram.org must go through cURL with HTTP code and json_last_error validation.
// Пример cURL для тестирования отправки сообщения
curl -s -X POST \
http://api.telegram.org/bot/sendMessage \
-H "Content-Type: application/json" \
-d '{\