Laravel Telegram Bot 2.11.0 Help

Webhooks

This package supports both sides of Telegram webhooks:

  • outgoing Bot API methods such as setWebhook, deleteWebhook, and getWebhookInfo;

  • an optional Laravel webhook receiver route for incoming Telegram Update payloads.

Primary Telegram references:

Configuration

Publish the package config and set the webhook values:

TELEGRAM_WEBHOOK_BOT=default TELEGRAM_BOT_LOGGING_ENABLED=true TELEGRAM_WEBHOOK_SECRET_TOKEN=change-this-secret TELEGRAM_WEBHOOK_REQUIRE_SECRET=true TELEGRAM_WEBHOOK_ROUTE_ENABLED=true TELEGRAM_WEBHOOK_ROUTE_URI=telegram-bot/webhook TELEGRAM_WEBHOOK_ROUTE_NAME=telegram-bot.webhook

config/telegram-bot.php contains:

'logging' => [ 'enabled' => env('TELEGRAM_BOT_LOGGING_ENABLED', true), ], 'webhook' => [ 'bot' => env('TELEGRAM_WEBHOOK_BOT', env('TELEGRAM_BOT', 'default')), 'bot_username' => env('TELEGRAM_WEBHOOK_BOT_USERNAME'), 'secret_token' => env('TELEGRAM_WEBHOOK_SECRET_TOKEN'), 'require_secret' => env('TELEGRAM_WEBHOOK_REQUIRE_SECRET', env('APP_ENV') === 'production'), 'handler' => App\Telegram\TelegramWebhookHandler::class, 'handlers' => [ 'message' => App\Telegram\Handlers\MessageHandler::class, 'callback_query' => App\Telegram\Handlers\CallbackQueryHandler::class, ], 'commands' => [ 'start' => App\Telegram\Commands\StartCommand::class, ], 'middleware' => [ App\Telegram\Middleware\ResolveTelegramTenant::class, ], 'fallback_handler' => App\Telegram\Handlers\FallbackHandler::class, 'dispatch_event' => true, 'route' => [ 'enabled' => env('TELEGRAM_WEBHOOK_ROUTE_ENABLED', true), 'uri' => env('TELEGRAM_WEBHOOK_ROUTE_URI', 'telegram-bot/webhook'), 'name' => env('TELEGRAM_WEBHOOK_ROUTE_NAME', 'telegram-bot.webhook'), 'middleware' => [], ], ], 'conversation' => [ 'enabled' => env('TELEGRAM_CONVERSATION_ENABLED', false), 'store' => env('TELEGRAM_CONVERSATION_STORE'), 'ttl' => env('TELEGRAM_CONVERSATION_TTL', 86400), 'key_prefix' => env('TELEGRAM_CONVERSATION_KEY_PREFIX', 'telegram-bot:conversation'), ],

The route defaults to POST /telegram-bot/webhook and is protected by X-Telegram-Bot-Api-Secret-Token when secret_token is configured. When require_secret is true, the middleware rejects webhook requests if no secret is configured; this defaults to true when APP_ENV=production.

The configured secret token is validated against Telegram's contract before it is accepted: 1-256 characters, using only A-Z, a-z, 0-9, _, and -. Invalid configured secrets fail closed and are not logged.

Register The Webhook With Telegram

Call setWebhook from a deployment command, Tinker, or your own release automation:

use AlexItDev91\LaravelTelegramBot\Facades\TelegramBot; TelegramBot::bot('default')->setWebhook([ 'url' => route('telegram-bot.webhook'), 'secret_token' => config('telegram-bot.webhook.secret_token'), 'allowed_updates' => [ 'message', 'callback_query', 'my_chat_member', ], ]);

The secret token must be 1-256 characters and may contain only A-Z, a-z, 0-9, _, and -.

The same operation is available through an interactive Artisan command:

php artisan telegram-bot:webhook:set

Non-interactive example:

php artisan telegram-bot:webhook:set \ --bot=default \ --url=https://example.com/telegram-bot/webhook \ --secret="${TELEGRAM_WEBHOOK_SECRET_TOKEN}" \ --allowed-updates=message \ --allowed-updates=callback_query

Use getWebhookInfo() to inspect webhook status:

$info = TelegramBot::bot('default')->getWebhookInfo();

Or:

php artisan telegram-bot:webhook:info --bot=default

Use deleteWebhook() to switch back to getUpdates:

TelegramBot::bot('default')->deleteWebhook([ 'drop_pending_updates' => false, ]);

Or:

php artisan telegram-bot:webhook:delete --bot=default --yes

See Console commands for all command options.

Handle Incoming Updates

Create a handler class:

namespace App\Telegram; use AlexItDev91\LaravelTelegramBot\Contracts\TelegramWebhookHandler as TelegramWebhookHandlerContract; use AlexItDev91\LaravelTelegramBot\DTO\TelegramWebhookUpdate; use AlexItDev91\LaravelTelegramBot\Laravel\TelegramWebhookReply; final readonly class TelegramWebhookHandler implements TelegramWebhookHandlerContract { public function handle(TelegramWebhookUpdate $update, string $botName): mixed { if ($update->type() !== 'message') { return null; } $text = $update->get('message.text'); if ($text === '/start') { return TelegramWebhookReply::fromUpdate($update)->text('Ready.'); } return null; } }

Then register it in config/telegram-bot.php:

'handler' => App\Telegram\TelegramWebhookHandler::class,

The handler may return:

  • null or true: the package returns {"ok": true};

  • TelegramWebhookReply: the package returns Telegram-compatible JSON such as {"method":"sendMessage","chat_id":"123","text":"Ready."};

  • an array: the package returns it as JSON;

  • a string: the package returns it as a plain response;

  • a Symfony/Laravel Response: the package returns it directly.

Use TelegramWebhookReply::text($text, chatId: $chatId), TelegramWebhookReply::method('sendChatAction', [...]), or TelegramWebhookReply::fromUpdate($update)->answerCallback('Saved.') when the reply can be sent as Telegram's synchronous webhook response. Webhook replies cannot upload InputFile objects; send files and slow work through an injected bot client or queued job instead. When webhook queueing is enabled, handler return values are processed by the queued job after Telegram already received {"ok": true, "queued": true}, so use the bot client for outbound messages in queued handlers.

Telegram only requires a successful 2xx response. Keep webhook handlers fast; dispatch jobs for slow work.

Dispatcher, Commands, And Fallbacks

For larger bots, leave webhook.handler as null and use the built-in dispatcher maps:

'webhook' => [ 'bot_username' => env('TELEGRAM_WEBHOOK_BOT_USERNAME'), 'handler' => null, 'handlers' => [ 'message' => App\Telegram\Handlers\MessageHandler::class, 'callback_query' => App\Telegram\Handlers\CallbackQueryHandler::class, 'pre_checkout_query' => App\Telegram\Handlers\PreCheckoutHandler::class, ], 'commands' => [ 'start' => App\Telegram\Commands\StartCommand::class, 'help' => App\Telegram\Commands\HelpCommand::class, ], 'fallback_handler' => App\Telegram\Handlers\FallbackHandler::class, ],

Command handlers implement TelegramWebhookCommandHandler:

namespace App\Telegram\Commands; use AlexItDev91\LaravelTelegramBot\Contracts\TelegramWebhookCommandHandler; use AlexItDev91\LaravelTelegramBot\DTO\TelegramWebhookUpdate; use AlexItDev91\LaravelTelegramBot\Laravel\TelegramWebhookCommand; use AlexItDev91\LaravelTelegramBot\TelegramBot; final readonly class StartCommand implements TelegramWebhookCommandHandler { public function __construct( private TelegramBot $telegram, ) { } public function handle(TelegramWebhookCommand $command, TelegramWebhookUpdate $update, string $botName): mixed { $chatId = $command->message()->chat()?->id(); if ($chatId !== null) { $this->telegram->bot($botName)->sendMessage([ 'chat_id' => (string) $chatId, 'text' => 'Ready.', ]); } return ['ok' => true]; } }

The dispatcher checks commands before update-type handlers. It understands /start, /start arguments, and /start@YourBot arguments; when bot_username is configured, commands addressed to another bot are ignored and normal update dispatch continues.

Update-type and fallback handlers implement TelegramWebhookHandler, the same contract used by the single-handler mode. The handlers map uses Telegram update field names such as message, callback_query, pre_checkout_query, poll, and chat_member. A * key may be used as a catch-all command or update handler.

Webhook Middleware

Use telegram-bot.webhook.middleware for application-level webhook pipeline steps that should run before the configured handler, dispatcher, command map, or fallback. This is separate from Laravel route middleware: route middleware protects the HTTP endpoint, while webhook middleware receives the parsed TelegramWebhookUpdate.

namespace App\Telegram\Middleware; use AlexItDev91\LaravelTelegramBot\Contracts\TelegramWebhookMiddleware; use AlexItDev91\LaravelTelegramBot\DTO\TelegramWebhookUpdate; use Closure; final class ResolveTelegramTenant implements TelegramWebhookMiddleware { public function process(TelegramWebhookUpdate $update, string $botName, Closure $next): mixed { app()->instance('telegram.tenant_key', (string) ($update->effectiveChat()?->id() ?? $botName)); return $next($update, $botName); } }

Register middleware in config:

'webhook' => [ 'middleware' => [ App\Telegram\Middleware\ResolveTelegramTenant::class, ], ],

Middleware runs in the configured order. It can short-circuit downstream handlers by returning its own result instead of calling $next($update, $botName).

Manual Route Registration

The package auto-registers the configured webhook route by default. If you disable auto-registration, use the Laravel route macro from your own route file:

use Illuminate\Support\Facades\Route; Route::telegramBotWebhook( uri: 'telegram/custom-webhook', name: 'telegram.custom-webhook', middleware: ['api'], );

The macro attaches the same VerifyTelegramWebhookSecret middleware as the default package route.

Events

When dispatch_event is true, every valid update dispatches:

AlexItDev91\LaravelTelegramBot\Laravel\Events\TelegramWebhookReceived

Example listener registration:

use AlexItDev91\LaravelTelegramBot\Laravel\Events\TelegramWebhookReceived; use Illuminate\Support\Facades\Event; Event::listen(TelegramWebhookReceived::class, function (TelegramWebhookReceived $event): void { $type = $event->update->type(); $bot = $event->botName; });

Use either a configured handler, events, or both. If both are enabled, the event is dispatched before the handler result is converted into the HTTP response.

TelegramWebhookUpdate

TelegramWebhookUpdate keeps the raw payload and detects all Bot API 10.0 update fields:

$update->updateId(); // 123456 $update->type(); // message, callback_query, guest_message, ... $update->data(); // payload for the detected type $update->has('message'); // bool $update->get('message.text'); // nested access with dot notation $update->payload(); // full raw update payload

For common Telegram objects, use typed accessors when you want IDE-friendly webhook code while keeping the raw payload available:

$message = $update->effectiveMessage(); $chat = $update->effectiveChat(); $user = $update->effectiveUser(); $callbackQuery = $update->callbackQuery(); $inlineQuery = $update->inlineQuery(); $shippingQuery = $update->shippingQueryData(); $preCheckoutQuery = $update->preCheckoutQueryData(); $chatMember = $update->chatMember(); $poll = $update->poll(); $pollAnswer = $update->pollAnswer(); $reaction = $update->messageReaction(); $chatBoost = $update->chatBoost(); $businessConnection = $update->businessConnection(); $managedBot = $update->managedBot(); $message?->messageId(); // int|null $message?->messageThreadId(); // int|null $message?->text(); // string|null $message?->caption(); // string|null $message?->replyToMessage(); // TelegramMessageData|null $message?->photoData(); // list<TelegramPhotoSizeData> $message?->documentData(); // TelegramDocumentData|null $message?->entitiesData(); // list<TelegramMessageEntityData> $message?->successfulPaymentData(); // TelegramSuccessfulPaymentData|null $chat?->id(); // int|string|null $chat?->type(); // private, group, supergroup, channel, ... $user?->id(); // int|string|null $user?->username(); // string|null $callbackQuery?->id(); // string|null $callbackQuery?->data(); // string|null $callbackQuery?->message(); // TelegramMessageData|null $inlineQuery?->query(); // string|null $shippingQuery?->invoicePayload(); // string|null $preCheckoutQuery?->totalAmount(); // int|null $preCheckoutQuery?->orderInfoData(); // TelegramOrderInfoData|null $chatMember?->newChatMemberData(); // TelegramChatMemberData|null $poll?->question(); // string|null $pollAnswer?->optionIds(); // list<int> $reaction?->newReaction(); // list<array<string, mixed>> $chatBoost?->boostData(); // TelegramChatBoostData|null $businessConnection?->isEnabled(); // bool|null $managedBot?->bot(); // TelegramUserData|null

Direct message-like accessors are also available: message(), editedMessage(), channelPost(), editedChannelPost(), businessMessage(), editedBusinessMessage(), and guestMessage(). Callback query updates are available through callbackQuery(), including typed from() and message() accessors. Inline mode is covered by inlineQuery() and chosenInlineResult(). Payment queries keep their backward-compatible array accessors and add typed shippingQueryData() and preCheckoutQueryData(). Common message media, entities, successful payments, order info, and chat member payloads also have typed object accessors such as photoData(), documentData(), entitiesData(), successfulPaymentData(), orderInfoData(), and newChatMemberData(). The remaining official update families are covered by businessConnection(), deletedBusinessMessages(), purchasedPaidMediaData(), poll(), pollAnswer(), messageReaction(), messageReactionCount(), chatBoost(), removedChatBoost(), and managedBot(). Membership updates are available through myChatMember(), chatMember(), and chatJoinRequest().

Unknown future update fields remain available through payload() and get() even before the SDK adds first-class awareness.

Common Handler Patterns

Message command:

$message = $update->effectiveMessage(); if ($message?->text() === '/start') { $this->telegram->bot($botName)->sendMessage([ 'chat_id' => (string) $message->chat()?->id(), 'text' => 'Ready.', ]); }

Callback button:

$callback = $update->callbackQuery(); if ($callback?->data() === 'menu:settings') { $this->telegram->bot($botName)->answerCallbackQuery([ 'callback_query_id' => $callback->id(), ]); }

Payment pre-checkout:

$query = $update->preCheckoutQueryData(); if ($query !== null) { $this->telegram->bot($botName)->answerPreCheckoutQuery([ 'pre_checkout_query_id' => $query->id(), 'ok' => true, ]); }

Chat member update:

$member = $update->chatMember(); if ($member?->newChatMemberData()?->status() === 'administrator') { // Grant app-side moderation permissions. }

Route And Middleware

The default route is registered by the service provider:

POST /telegram-bot/webhook

Customize it in config:

'route' => [ 'enabled' => true, 'uri' => 'integrations/telegram/webhook', 'name' => 'telegram.webhook', 'middleware' => ['throttle:telegram-webhook'], ],

Package middleware always validates X-Telegram-Bot-Api-Secret-Token when secret_token is configured. It also fails closed when require_secret is true and the secret is missing. Add rate limiting, IP filtering, or observability middleware in the middleware array when the host application needs it.

The receiver rejects malformed updates before dispatching events or handlers. A valid incoming payload must be JSON and include an integer update_id, matching Telegram's Update object contract.

Queue And Idempotency

By default, webhook handlers run synchronously during the HTTP request. For production handlers that do non-trivial work, enable the queue handoff:

TELEGRAM_WEBHOOK_QUEUE_ENABLED=true TELEGRAM_WEBHOOK_QUEUE_CONNECTION=redis TELEGRAM_WEBHOOK_QUEUE=telegram-webhooks TELEGRAM_WEBHOOK_QUEUE_AFTER_COMMIT=false

When queueing is enabled, the receiver validates the payload and secret token, dispatches AlexItDev91\LaravelTelegramBot\Laravel\Jobs\TelegramWebhookJob, and returns:

{"ok": true, "queued": true}

The queued job processes the same TelegramWebhookUpdate through the configured event, handler, dispatcher, command, and fallback pipeline. If the host application does not provide Laravel's bus dispatcher, the receiver logs a warning and processes the update synchronously instead of dropping it.

Telegram can retry webhook deliveries, so enable the cache-backed idempotency guard when handlers are not safe to run twice for the same update_id:

TELEGRAM_WEBHOOK_IDEMPOTENCY_ENABLED=true TELEGRAM_WEBHOOK_IDEMPOTENCY_STORE=redis TELEGRAM_WEBHOOK_IDEMPOTENCY_TTL=86400

The guard uses the host application's cache repository and add() semantics. A repeated update for the same bot and update_id returns:

{"ok": true, "duplicate": true}

If a synchronous handler throws, the idempotency key is released so Telegram retries can be processed. In queued mode, the key is kept after the job is dispatched and Laravel queue retries handle processing failures.

Conversation State

The package includes an opt-in cache-backed conversation store for stateful webhook flows such as profile wizards, support triage, and multi-step admin commands.

TELEGRAM_CONVERSATION_ENABLED=true TELEGRAM_CONVERSATION_STORE=redis TELEGRAM_CONVERSATION_TTL=86400 TELEGRAM_CONVERSATION_KEY_PREFIX=telegram-bot:conversation

Resolve TelegramConversationManager in a handler or middleware. For guided forms, build a TelegramConversationWizard on the update-scoped workflow:

use AlexItDev91\LaravelTelegramBot\DTO\TelegramWebhookUpdate; use AlexItDev91\LaravelTelegramBot\Laravel\Conversation\TelegramConversationWizard; use AlexItDev91\LaravelTelegramBot\Laravel\TelegramConversationManager; final readonly class ProfileWizardHandler { public function __construct(private TelegramConversationManager $conversations) { } public function handle(TelegramWebhookUpdate $update, string $botName): mixed { $chatId = $update->effectiveChat()?->id(); if ($chatId === null) { return ['ok' => true]; } $wizard = TelegramConversationWizard::for($this->conversations->workflowForUpdate($update, $botName)) ->timeout(600) ->cancelledMessage('Profile setup cancelled.'); $wizard->step('email', 'email') ->prompt('Send your support email address.') ->invalid('That does not look like an email address. Try again.') ->validate(static fn (mixed $value): bool => is_string($value) && str_contains($value, '@')) ->complete('Profile email saved.'); $result = $wizard->handle($update); return $result->hasMessage() ? ['method' => 'sendMessage', 'chat_id' => $chatId, 'text' => $result->message()] : ['ok' => true]; } }

TelegramConversationWizard starts the first step automatically, stores validated values in TelegramConversationContext, returns a result object with reply text, supports /cancel and /back, and uses callback-query data for inline keyboard steps. Use TelegramConversationWorkflow directly when you need a custom state machine with guarded transitions or non-linear storage rules.

Conversation keys are namespaced by bot and the effective chat/user when Telegram provides them. If a cache repository is unavailable, the manager returns DTOs for the current call but does not persist state, and the package logs a safe warning when logging is enabled.

Human Handoff

TelegramHumanHandoff is an optional conversation contract for pausing automation while a support operator works with the user in a private chat, group, forum topic, or external ticket system.

use AlexItDev91\LaravelTelegramBot\Laravel\Handoff\TelegramHumanHandoff; $workflow = $this->conversations->workflowForUpdate($update, $botName); if (TelegramHumanHandoff::fromWorkflow($workflow) !== null) { return ['ok' => true]; } $handoff = TelegramHumanHandoff::fromUpdate($update, 'needs-human', [ 'ticket_id' => $ticket->public_id, ]); $handoff->open($workflow, ['resume_state' => 'support-summary'], ttl: 86400); $this->telegram->channel('support')->sendMessage([ 'text' => $handoff->operatorText('Support handoff'), ]);

Close the handoff from the original user workflow after the operator resolves it:

$resumeState = $workflow->context()->string('resume_state'); TelegramHumanHandoff::close($workflow); if ($resumeState !== null) { $workflow->start($resumeState); }

Store the original workflow key or ticket ID in the host app when operators resolve cases from another chat. Queue operator notifications for busy support inboxes, keep raw customer text and attachments out of summaries unless necessary, restrict operator chat membership, and keep bot tokens, webhook secrets, and payment data out of tickets and logs.

Observability Events

When telegram-bot.webhook.dispatch_event is true, the Laravel integration dispatches:

  • AlexItDev91\LaravelTelegramBot\Laravel\Events\TelegramWebhookReceived

  • AlexItDev91\LaravelTelegramBot\Laravel\Events\TelegramWebhookHandled

  • AlexItDev91\LaravelTelegramBot\Laravel\Events\TelegramWebhookFailed

  • AlexItDev91\LaravelTelegramBot\Laravel\Events\TelegramWebhookQueued

  • AlexItDev91\LaravelTelegramBot\Laravel\Events\TelegramWebhookDuplicateSkipped

Use these events for metrics, tracing, and low-cardinality operational dashboards. Avoid logging full payloads, chat IDs, message text, tokens, and secret headers from event listeners.

Logging

When telegram-bot.logging.enabled is true, the Laravel integration writes warning/error logs for:

  • rejected webhook secret tokens and missing required secret configuration;

  • invalid webhook update payloads;

  • invalid webhook handler configuration;

  • webhook handler failures before the exception is rethrown;

  • Telegram Bot API ok: false responses and transport response failures from Laravel-resolved bot clients.

Log context is intentionally limited to operational metadata such as method names, HTTP status codes, Telegram error codes, update IDs, update types, bot names, and exception classes. The package does not log bot tokens, webhook secret header values, request payloads, response bodies, chat IDs, or message text.

Security Checklist

  • Use HTTPS for public Telegram webhooks.

  • Set TELEGRAM_WEBHOOK_SECRET_TOKEN and pass the same value to setWebhook.

  • Keep TELEGRAM_WEBHOOK_REQUIRE_SECRET=true in production so a missing secret does not silently expose the route.

  • Keep TELEGRAM_BOT_LOGGING_ENABLED=true unless the host application has equivalent monitoring.

  • Do not commit real bot tokens, webhook secrets, chat IDs, logs, or payload dumps.

  • Avoid long-running work in the webhook request; use TELEGRAM_WEBHOOK_QUEUE_ENABLED=true.

  • Enable TELEGRAM_WEBHOOK_IDEMPOTENCY_ENABLED=true when duplicate webhook processing would be harmful.

  • Use allowed_updates in setWebhook to reduce unnecessary traffic.

  • Use getWebhookInfo after deployment to confirm Telegram sees the expected URL and has no delivery errors.

10 June 2026