Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Driver Telegram Laravel Package

botman/driver-telegram

Telegram driver for BotMan, the PHP chatbot framework. Connect your Laravel or plain PHP app to the Telegram Bot API and build conversational bots with commands, conversations, inline keyboards, and rich media through BotMan's expressive, framework-agnostic API.

Deep Wiki
Context7

Getting Started

1. Install the driver

composer require botman/driver-telegram

This pulls in botman/botman (^2.0) if it isn't already there.

2. Wire it up

  • Laravel (BotMan Studio or manual service provider): the driver ships a config file at config/botman/telegram.php with a token key. Point it at TELEGRAM_TOKEN in .env and run php artisan config:clear.
  • Plain PHP: you must register the driver before constructing BotMan, or you'll get a “Driver [Telegram] not supported” exception:
use BotMan\BotMan\BotManFactory;
use BotMan\BotMan\Drivers\DriverManager;
use BotMan\Drivers\Telegram\TelegramDriver;

DriverManager::loadDriver(TelegramDriver::class);
$botman = BotManFactory::create($config);

3. Register the webhook

php artisan botman:telegram:register

or manually hit https://api.telegram.org/bot<TOKEN>/setWebhook?url=https://your-app.test/botman. Verify with getWebhookInfo. For local dev, expose your app with ngrok/expose — Telegram requires a valid HTTPS endpoint and will silently fail on self-signed certs.

4. First use case: /start

$bot->hears('/start', function (BotMan $bot) {
    $bot->reply('Welcome! Try /help.');
});

$bot->hears('/help', function (BotMan $bot) {
    $bot->reply('Commands: /start, /help');
});

Where to look first

  • config/botman/telegram.php — token and driver config.
  • BotMan\Drivers\Telegram\TelegramDriver — the class that maps Telegram updates into BotMan messages; read loadMessages() and sendPayload() to understand what's supported.
  • BotMan\Drivers\Telegram\Extensions\Keyboard, KeyboardButton, and Button — the only sane way to build inline/reply keyboards.
  • BotMan\Drivers\Telegram\TelegramPhotoDriver (and siblings TelegramAudioDriver, TelegramVideoDriver, TelegramFileDriver, TelegramVoiceDriver, TelegramLocationDriver, TelegramContactDriver) — these are what make $bot->receivesImages(), receivesVideos(), etc. work only for Telegram.
  • The routes file / BotManServiceProvider — defaults to a POST /botman webhook route.

First realistic milestone: a working webhook handler that answers /start, plus one inline-keyboard menu. That exercises the whole chain (route → driver → middleware → hears) before you add conversations.

Implementation Patterns

Keyword listeners and pattern matching

BotMan's hears() takes a regex. Anchor it, or /^start/ will also match "restarting".

$bot->hears('^/order (\d+)$', function (BotMan $bot, $orderId) {
    $bot->reply("Looking up order {$orderId}…");
});

Conversations for multi-step flows

Conversations are the right tool for anything stateful (checkout, onboarding, forms). Extend Conversation, implement run(), and use $this->ask():

class OnboardConversation extends Conversation
{
    public function askName()
    {
        $this->ask('What should I call you?', function (Answer $answer) {
            $this->name = $answer->getText();
            $this->askEmail();
        });
    }
}

State lives in the BotMan cache. On more than one app server, switch the cache driver to Redis, or a conversation started on node A and continued on node B will reset.

Inline keyboards + callbacks

Build with the provided extension classes and pass the array as the second argument to reply():

use BotMan\Drivers\Telegram\Extensions\Keyboard;
use BotMan\Drivers\Telegram\Extensions\KeyboardButton;

$keyboard = Keyboard::create()
    ->type(Keyboard::TYPE_INLINE)
    ->addRow(KeyboardButton::create('Confirm')->callbackData('confirm'))
    ->addRow(KeyboardButton::create('Cancel')->callbackData('cancel'))
    ->toArray();

$bot->reply('Proceed?', $keyboard);

Callback presses arrive as updates whose text is the callback_data, so handle them with the same hears() matcher you'd use for typed commands:

$bot->hears('^confirm$', function (BotMan $bot) {
    // ...
});

Callback queries also need answering via answerCallbackQuery, otherwise the user sees a spinning loader. If the driver version you're on doesn't auto-answer, call the Telegram API directly (see extension points below).

Reply keyboards (persistent buttons)

Use Keyboard::TYPE_KEYBOARD with ->resizeKeyboard(), ->oneTimeKeyboard(). Remember they replace the user's keyboard — always provide a way back (->addRow(KeyboardButton::create('Menu'))).

Sending media

Compose an OutgoingMessage with an attachment; the driver routes to the correct Telegram endpoint:

$message = OutgoingMessage::create('Here is your receipt')
    ->withAttachment(Image::url($signedUrl));
$bot->reply($message);

Telegram fetches media by URL, so the URL must be publicly reachable — signed S3 URLs work, localhost storage paths don't. Captions are capped at 1024 chars; long text needs a separate message.

Formatting

Pass parse_mode as an additional parameter:

$bot->reply("<b>{$name}</b>, your order shipped.", ['parse_mode' => 'HTML']);

HTML is usually safer than Markdown (Markdown v1 chokes on stray underscores in usernames).

Middleware for cross-cutting concerns

Register middleware in config/botman/config.php (or BotManFactory config) for rate limiting, allow-listing chat IDs, logging the raw update, and injecting tenant context:

class OnlyAllowListedChats
{
    public function handle(IncomingMessage $message, $next, BotMan $bot)
    {
        if (! in_array($message->getSender(), config('bot.allowed_ids'))) {
            return null; // stop processing
        }
        return $next($message);
    }
}

The driver's group middleware hook is the cleanest place to enforce “this bot only responds in the private DM channel of the right tenant”, rather than repeating checks per listener.

Webhook vs. polling

Webhooks are the default for Laravel: fast, no daemon, but each request blocks a PHP-FPM worker. Dispatch slow work (CRM updates, PDF generation) to a queue and reply immediately — Telegram will retry the webhook if you exceed its timeout, and you'll get duplicate handling without idempotency keys.

Polling is a custom Artisan command running a listen() loop with sleep(1) and a distributed lock so two workers don't call getUpdates concurrently (Telegram will 409-conflict). Useful for dev machines behind firewalls; rarely worth it in production versus a tunnel.

Persisting Telegram-specific data

BotMan normalises everything into IncomingMessage. For things it doesn't expose (message_id, chat type, callback query ID, reply-to-message), grab the raw payload:

$raw = $bot->getMessage()->getPayload();
$chatId = $raw['chat']['id'];

Store chat_id rather than user ID — group chats and supergroups deliver a different chat.id than the sender's id, and broadcasting to users should go through the chat, not the user.

Gotchas and Tips

  • The webhook returns 200 even when your handler fatals. BotMan's exception handling swallows errors, and Telegram reads the 200 as “delivered”. Put a try/catch with Log::error() at the top of your handler or in middleware; don't trust “no errors in Telegram” as success.
  • Telegram retries unacknowledged updates. If your handler is slow or 500s, the same update arrives again. Make handlers idempotent keyed on update_id/message_id — a cache add() with a TTL is enough.
  • Missing DriverManager::loadDriver() is the single most common error in non-Laravel setups: “Driver [Telegram] not supported”. In Laravel it's the service provider's job, but if you registered providers selectively, check it.
  • config:cache and tokens. After editing TELEGRAM_TOKEN, run php artisan config:cache (or config:clear) — a cached config will keep hitting the old token with no obvious error.
  • parse_mode + user input = broken messages. A user typing <3 in HTML mode produces a Telegram 400 (“can't parse entities”). Run user-supplied strings through htmlspecialchars() (or e()), or send without a parse mode and accept plain text.
  • Rate limits are real. Roughly 30 messages/sec globally and ~1 msg/sec per chat. Bulk sends need usleep() or BotMan's typesAndWaits(). Blasting 500 users in a foreach will get you a 429 with a retry_after you're probably ignoring.
  • 4096-character limit per message and 1024 per caption. Split long responses; don't let a str_repeat in a test blow up in production.
  • 409 Conflict from getUpdates means either two pollers are running or a webhook is still registered. deleteWebhook before switching to polling; setWebhook before switching back.
  • Webhook secret validation isn't automatic. Pass secret_token when registering and check the X-Telegram-Bot-Api-Secret-Token header in a middleware. Without it, anyone who learns your URL can feed you fake updates.
  • Inline keyboard callbacks and the spinner. Unanswered callback queries time out and leave a loading state; answer them explicitly. If your driver version doesn't expose it, call answerCallbackQuery via the driver's HTTP helper (below).
  • Media groups (albums) arrive as separate updates, not one. Don't build logic that assumes one message per upload.
  • Group privacy mode. A bot added to a group only receives commands (/foo) and replies unless privacy mode is disabled in BotFather — that's why your “bot ignores normal messages in groups” bug isn't a bug.
  • Old package, newer Bot API. Last release was 2020, so Bot API features since then (forum topics, message_thread_id, spoilers, custom emoji, link_preview_options) aren't mapped. You can still reach them: the base HttpDriver exposes sendRequest($endpoint, $parameters), so $bot->getDriver()->sendRequest('sendMessage', [...]) handles raw calls. Wrap those in a small service class rather than scattering them across listeners.
  • Extending the driver. For anything structural, subclass TelegramDriver, override loadMessages()/sendPayload(), and bind your subclass into the container so BotMan resolves it instead of the stock one. Keeps vendor upgrades (if they ever happen) merges clean.
  • Attachment matching is driver-specific. $bot->receivesImages() only fires because TelegramPhotoDriver is registered. If photo handling mysteriously stops, check you haven't replaced the driver list in config.
  • Conversation state + multiple servers. The default file cache is fine for one box and broken for two. Redis (or any shared store) is the fix; don't discover this after an autoscaling event.
  • Test against a real bot, not mocks. Spin up a throwaway bot via BotFather, point a tunnel at your app, and keep a botman:tinker session open. The driver's payload normalisation is the part most worth verifying by hand — payload shapes differ for edited messages, channel posts, and callbacks in ways unit tests miss.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource