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

Telegram Bot Laravel Package

longman/telegram-bot

longman/telegram-bot is a mature PHP library for building Telegram bots with a clean object-oriented API. It supports commands, inline keyboards, conversations, admin tools, webhooks and long polling, with extensive docs and examples for rapid bot development. Suitable for simple scripts and large b...

View on GitHub
Deep Wiki
Context7

Getting Started

longman/telegram-bot is framework-agnostic PHP — there is no Laravel service provider, facade, config publish, or migrations. You wire it up yourself, which takes about 30 minutes.

composer require longman/telegram-bot:^0.83.1

Pin to 0.83.1 or later: 0.83.0 shipped a critical error that 0.83.1 fast-fixes (PR #1467). If you were on 0.82.x and upgrading, 0.83.1 also folds in a fix that makes setCustomBotApiUri() actually take effect when the Telegram object has already been constructed (PR #1296).

Requirements: PHP 8.1+, ext-curl, ext-mbstring, ext-json, and MySQL/MariaDB only if you want the DB-backed features (update logging, Conversation, inline-query persistence, the rate limiter).

Create a config file. Copy the package's config.php example into config/telegram.php and pull secrets from .env:

return [
    'api_key'      => env('TELEGRAM_BOT_TOKEN'),
    'bot_username' => env('TELEGRAM_BOT_USERNAME'),
    'commands'     => [
        'paths' => [app_path('Telegram/Commands')],
    ],
    'admins' => [env('TELEGRAM_ADMIN_ID')],
    'mysql'  => [
        'host'     => env('DB_HOST'),
        'user'     => env('DB_USERNAME'),
        'password' => env('DB_PASSWORD'),
        'database' => env('DB_DATABASE'),
    ],
];

Bind a singleton in a small TelegramServiceProvider:

$this->app->singleton(Telegram::class, function () {
    $config   = config('telegram');
    $telegram = new Telegram($config['api_key'], $config['bot_username']);

    foreach ($config['commands']['paths'] as $path) {
        $telegram->addCommandsPath($path, 'App\\Telegram\\Commands');
    }

    $telegram->enableAdmins($config['admins']);
    $telegram->setDownloadPath(storage_path('app/telegram/downloads'));

    // Optional: self-hosted Bot API server (needed for >20MB files)
    if ($config['api_uri'] ?? null) {
        $telegram->setCustomBotApiUri($config['api_uri'], $config['api_key']);
    }

    return $telegram;
});

Add a webhook route in routes/api.php (stateless, so no CSRF token to fight) and a thin controller that delegates to $telegram->handle() — see the next section.

Where to look first, in order: the repo's examples/ folder (set.php, unset.php, hook.php), then src/Commands/ to see one class per command type, then structure.sql if you plan to use conversations. The README's "Commands" section is the real documentation.

First use case — a /start handler:

namespace App\Telegram\Commands;

use Longman\TelegramBot\Commands\UserCommand;
use Longman\TelegramBot\Entities\ServerResponse;

class StartCommand extends UserCommand
{
    protected $name = 'start';
    protected $description = 'Greet the user';
    protected $usage = '/start';

    public function execute(): ServerResponse
    {
        $from = $this->getMessage()->getFrom();

        return $this->replyToChat("Hi {$from->getFirstName()}, welcome aboard.");
    }
}

Register the webhook once from an Artisan command or tinker: $telegram->setWebhook(route('telegram.webhook'), ['secret_token' => config('telegram.secret')]).

Implementation Patterns

Thin webhook controller, heavy work queued. Telegram retries any update that isn't answered with a 200 within ~60s, so the controller should only parse and dispatch:

public function __invoke(Telegram $telegram)
{
    try {
        $response = $telegram->handle();
        return response()->json($response->getRawData(), $response->isOk() ? 200 : 500);
    } catch (\Throwable $e) {
        Log::error('Telegram webhook failed', ['e' => $e]);
        return response()->noContent(); // still 200 — avoid a retry storm
    }
}

For commands that hit your database or an external API, resolve the command's intent in the webhook and push the actual reply onto a queue job that calls Request::sendMessage().

Pick the right command base class. UserCommand for /commands and plain text, SystemCommand for service messages (new members, left chat, pinned), AdminCommand for restricted ops (matched against enableAdmins), InlineCommand for @bot query, CallbackqueryCommand for inline keyboard presses. A GenericmessageCommand with $name = 'genericmessage' acts as the catch-all for text that matched nothing — it's the cleanest place to implement conversation fallbacks.

Outbound sends from anywhere. Request is a static API surface that mirrors the Bot API one-to-one:

$response = Request::sendMessage([
    'chat_id'    => $user->telegram_id,
    'text'       => $text,
    'parse_mode' => 'HTML',
    'reply_markup' => json_encode(['inline_keyboard' => [[
        ['text' => 'Approve', 'callback_data' => "approve:{$order->id}"],
    ]]]),
]);

if (! $response->isOk()) {
    Log::warning('Telegram send failed', [
        'code' => $response->getErrorCode(),
        'desc' => $response->getDescription(),
    ]);
}

Wrapping that in a Laravel notification channel is the highest-leverage integration: one TelegramChannel::send() that stores the chat ID and lets you use Notification::route('telegram', $id) everywhere.

Conversations for multi-step flows. Longman\TelegramBot\Conversation stores state in the conversation table keyed by (user_id, chat_id, command):

$conversation = new Conversation($user_id, $chat_id, $this->getName());

if (! isset($conversation->notes['step'])) {
    $conversation->notes['step'] = 'awaiting_email';
    $conversation->update();
}

// ... on receiving the next message:
$conversation->notes['email'] = $text;
$conversation->notes['step'] = 'confirm';
$conversation->update();

// when finished:
$conversation->stop();

This is genuinely useful but only after you've created the tables.

Self-hosted Bot API server. setCustomBotApiUri() points every Request:: call at your own telegram-bot-api instance, which is the only way to send/download files larger than the cloud limits (20MB download / 50MB upload). Set it inside the singleton binding so it's applied before any request fires — 0.83.1 makes the call stick even on an already-instantiated Telegram object, but the ordering habit is still the safe one.

Long polling for local dev. Skip ngrok while iterating: $telegram->handleGetUpdates() on a loop, driven by an Artisan command under supervisor. Webhook and getUpdates are mutually exclusive — you'll get a 409 until you deleteWebhook().

Turn on the limiter with $telegram->enableLimiter() if you broadcast. It tracks per-chat message counts in the DB and throttles to Telegram's published limits instead of letting you eat 429s.

Gotchas and Tips

The static Request object holds whichever Telegram instance was constructed most recently. In a queue worker that's fine if you always resolve from the container. In tests or long-running pollers, construct once and be deliberate — otherwise requests silently go out under the wrong token. The same applies to setCustomBotApiUri(): because it mutates shared static state, calling it on a throwaway instance still affects everything downstream.

No Laravel migrations ship with the package — only raw SQL. Conversation, update logging, the limiter and inline-query storage all need tables. Fresh install: import structure.sql. Upgrading between minors: apply the matching versioned delta first (e.g. 0.82.0-0.83.0.sql, which 0.83.1 itself patched — re-pull it if you ran an earlier copy), then your app's own migrations. There's no php artisan vendor:publish; hand-write a migration or import the SQL. The Conversation class throws immediately without them, and the error doesn't hint at the missing table.

Command naming is filename-driven and case-sensitive. /foo resolves to FooCommand, whose $name property must be 'foo'. If you add your own commands path, pass the matching namespace as the second argument to addCommandsPath() — a mismatch shows up as a silent "command not found" rather than a PHP error.

Errors are return values, not exceptions. Request::sendMessage() returns a ServerResponse; check isOk(). Telegram-level errors (blocked user, chat not found, bad HTML) come back in getErrorCode()/getDescription(). Only transport failures raise TelegramException. Wrap outbound calls in a helper that logs the description — otherwise you'll chase phantom bugs for an afternoon.

Escape HTML/Markdown yourself. With parse_mode => 'HTML' any stray < or & in user-supplied text breaks the send with a 400. htmlspecialchars($text, ENT_QUOTES) before interpolation, always.

Respect the 4096-character message limit and 1024 for captions. Chunk broadcasts at word boundaries; the library will not truncate for you.

Answer callback queries within 10 seconds via Request::answerCallbackQuery() or the client-side spinner hangs forever and users tap repeatedly.

Keep api_key out of cached config drift. If you rotate the token, php artisan config:clear — a stale bootstrap/cache/config.php will keep the old bot alive and you'll swear the webhook is broken.

Watch the Bot API version lag. The library's typed methods are generated and trail Telegram's releases by months. If you need a brand-new endpoint, check whether a Request:: method exists in the installed version before designing around it; otherwise fall back to a raw Guzzle call with your own payload.

setDownloadPath()/setUploadPath() must exist and be writable by the PHP-FPM/queue user, and they're plain filesystem paths — they do not go through Laravel's Storage disk abstraction. Point them at storage/app/... and add cleanup, or inbounds files will accumulate forever.

Test commands by building Update entities from fixture JSON and asserting on the returned ServerResponse. That's faster and more stable than mocking HTTP, and it keeps your command classes free of Laravel request coupling.

Extension points worth knowing: a custom Command base class with shared reply()/guard() helpers, a subclassed Conversation for your own state schema, setCustomBotApiUri() for a local Bot API server, and TelegramLog::initErrorLog() if you want the library's own diagnostics separated from your application log channel.

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.
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
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager