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

Telebot Laravel Package

westacks/telebot

Westacks Telebot is a Laravel/PHP wrapper for the Telegram Bot API with an expressive, fluent interface for sending messages, media, keyboards, and handling updates. It integrates cleanly with Laravel and simplifies building bots and notifications.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up

composer require westacks/telebot

Requirements are modest: PHP 8.x with ext-json and ext-curl. The HTTP transport is bundled through the bot-api-base dependency, so there's no Guzzle/HTTP-client plumbing to configure before your first request.

Minimal bot (polling)

use WeStacks\TeleBot\TeleBot;

$bot = new TeleBot([
    'token' => getenv('TELEGRAM_BOT_TOKEN'),
]);

$bot->onMessage(function ($bot, $message) {
    $bot->sendMessage([
        'chat_id' => $message->chat->id,
        'text'    => $message->text ?? 'Send me text and I will echo it.',
    ]);
});

$bot->listen(); // long polling — no public URL needed

Run it from a terminal, message your bot, and you have a working loop in under five minutes.

Where to look first

  1. WeStacks\TeleBot\TeleBot — the entry point and config array (token, api_url, handler registration, request options). Read this constructor before anything else.
  2. WeStacks\TeleBot\Objects\ — typed wrappers for Update, Message, User, Chat, CallbackQuery. This directory is effectively the best documentation of what your handlers will receive, because it mirrors the Telegram Bot API schema exactly.
  3. WeStacks\TeleBot\Handlers\ — the base Handler plus the built-in narrowing handlers (CommandHandler, TextHandler, CallbackQueryHandler, etc.). Subclass these rather than writing raw closures once you have more than two behaviours.
  4. The method surface is the Telegram Bot API itself — sendMessage, sendPhoto, editMessageText, answerCallbackQuery, getFile, and so on are all available on the bot object. The library doesn't invent a vocabulary; keep the official API docs open and translate method names to calls.

First real use case

Start with a /start welcome plus a single inline keyboard, then a callback handler that edits the same message. That exercise touches all three things you'll use daily: sending, callback routing, and message editing.

Implementation Patterns

Dynamic API methods with array payloads

Every Bot API method is callable on the bot (and usually on handlers) by name, taking a single associative array that maps 1:1 to the API's JSON body:

$bot->sendMessage([
    'chat_id'      => $chatId,
    'text'         => '<b>Done</b>',
    'parse_mode'   => 'HTML',
    'reply_markup' => [
        'inline_keyboard' => [[
            ['text' => 'Open', 'callback_data' => 'open:42'],
        ]],
    ],
]);

reply_markup and other nested structures are plain arrays — no builder objects to learn. Return values are typed objects (or booleans for acknowledgment-style methods), so $sent->message_id works immediately after sendMessage.

Handler classes instead of closures

Once a bot has more than a couple of behaviours, move each to a handler class. The pattern is a gate plus an action:

use WeStacks\TeleBot\Handlers\CommandHandler;

class StartHandler extends CommandHandler
{
    protected $commands = ['start', 'help'];

    public function handle()
    {
        $this->sendMessage([
            'text' => 'Welcome. Try /report or just type a message.',
        ]);
    }
}

Register handlers in the config array, and order them most-specific-first — put a catch-all echo/fallback handler last, otherwise it swallows everything. Because handlers are ordinary classes resolved by the container-less bot, they're trivial to unit test: construct with a fixture Update object and assert on the payload you would have sent.

Separating "trigger" from "handle"

Keep the trigger()/matching logic pure (inspect the update, return a bool) and put all I/O in handle(). This makes routing bugs reproducible in tests without a network round-trip, and it keeps your handler list readable as a routing table.

Callback data as a router

Treat callback_data like a route string and switch on it, always sending an acknowledgment:

$bot->onCallbackQuery(function ($bot, $query) {
    [$action, $id] = explode(':', $query->data) + [null, null];

    $bot->answerCallbackQuery(['callback_query_id' => $query->id]);

    match ($action) {
        'open'    => $bot->editMessageText([
            'chat_id'    => $query->message->chat->id,
            'message_id' => $query->message->message_id,
            'text'       => "Opened #{$id}",
        ]),
        default   => null,
    };
});

Keep callback_data under 64 bytes — it's a hard API limit, not a soft guideline. Store the real state server-side and put only the key in the callback.

Stateless bots need external state

The library deliberately holds no conversation state. A multi-step flow (a wizard, a form, a checkout) is a row in your database or cache keyed by chat_id (or chat_id:user_id in groups), holding the current step and partial answers. A common Laravel shape:

Cache::put("tg:step:{$chatId}", 'awaiting_amount', now()->addMinutes(30));

// later, in the message handler
$step = Cache::get("tg:step:{$chatId}");

Don't try to encode step state into callback_data alone — it breaks the moment a user replies with a text message instead of tapping a button.

Webhook in a Laravel app

Bind a POST route, exclude it from CSRF, and hand the raw request body to the bot's webhook entry point (see the README for whether your version exposes run() or handle() for this — it's the one API detail worth verifying against the installed version):

// routes/api.php
Route::post('/telegram/webhook', TelegramWebhookController::class);
public function __invoke()
{
    $bot = new TeleBot(config('services.telegram'));
    $bot->handle(request()->all()); // confirm exact entry point for your version
    return response()->noContent();
}

Register the handler list in a service provider or a small factory class so both the webhook controller and your artisan commands get an identically configured bot.

Outbound sends from jobs and commands

The bot object is not tied to the request lifecycle. Instantiate a configured bot inside a queued job and send from there — this is the correct place for broadcasts, reminders, and anything that could exceed Telegram's webhook timeout:

class SendReportReady extends ShouldQueue
{
    public function handle(): void
    {
        app(TelegramBotFactory::class)->make()->sendMessage([
            'chat_id' => $this->chatId,
            'text'    => 'Your report is ready.',
        ]);
    }
}

Testing without hitting Telegram

Point api_url at a local stub (a tiny route that records the JSON body and returns a canned ok: true result) and assert on what your handler tried to send. This validates payload shape — chat IDs, parse_mode, keyboard structure — which is where most bugs actually live. Reserve real-token tests for webhook registration and file uploads.

Gotchas and Tips

409 Conflict on startup. Long polling and a registered webhook are mutually exclusive. If listen() dies with a conflict, you still have a webhook set — call deleteWebhook (or setWebhook with an empty url) before polling. Conversely, in production, remember that an accidentally running local poller will steal updates from your webhook.

Never leave a callback unanswered. Telegram shows a spinner on the tapped button until answerCallbackQuery arrives. Even if you do nothing else, acknowledge it — and answer before slow work, since the client gives up quickly.

Respond to webhooks in well under Telegram's timeout. Telegram retries non-2xx and slow responses, which means duplicate updates. Two defences: return 200 immediately and push real work to a queue, and deduplicate on update_id (a Cache::add("tg:update:{$updateId}", true, 3600) guard is enough).

Rate limits are per-chat, not just global. Roughly one message per second per chat, ~30/sec overall. Broadcasts to thousands of users need throttling (usleep, a rate-limited queue, or chunked jobs). On 429, read retry_after from the exception response and sleep that long rather than retrying immediately — otherwise you compound the block.

4096 characters is a hard ceiling. Long model output, HTML pages, and error dumps all exceed it. Split on paragraph boundaries, send sequentially, and consider sendMediaGroup for batches of images rather than N separate calls.

parse_mode plus user input equals broken messages. A stray < or _ in user-supplied text throws a parse error and the whole message fails. Escape with htmlspecialchars($text, ENT_QUOTES) for HTML mode, and when in doubt send plain text and use entities sparingly.

Uploads vs URLs behave differently. Passing a local path or stream uploads through your server (slower, and limited to ~50 MB via the Bot API). Passing a public URL lets Telegram fetch it directly — faster, but the URL must be reachable from Telegram's network, which rules out localhost and most private storage. If you need multi-gigabyte files, point api_url at a self-hosted Bot API server and note that its file limits are much higher.

Request timeouts must exceed the poll timeout. Long polling holds the connection open; if your HTTP client timeout is shorter than the timeout you pass to getUpdates (or the default), you'll see spurious timeouts and reconnect loops. Either shorten the poll timeout or lengthen the client timeout — never leave them crossed.

The token lives in the request URL. Telegram's API path is /bot<token>/method, so any exception, stack trace, or HTTP debug log that echoes the URL leaks your token into storage/logs. Scrub it in your exception handler, or wrap the bot in a decorator that catches and re-throws sanitized messages.

Version drift is real. This is a 309-star, effectively single-maintainer package — well-designed and small enough to read in an afternoon (vendor/westacks/telebot/src), but the handler-registration and entry-point API has moved between majors. Pin a major version in composer.json, read the upgrade notes before bumping, and treat the installed source as the authority over any blog post.

Typed objects are snapshots, not live proxies. $update->message->chat->id is a plain property read on a hydrated object. You cannot call API methods on them, and fields missing from the update (edited messages, channel posts, service messages) simply won't be set — guard with isset() or ?? rather than assuming the schema.

Extension points worth knowing. Subclass Handler for custom matching logic; override the TeleBot/Bot layer to inject cross-cutting concerns (structured logging, the update_id dedupe, metric counters) in one place instead of per handler; and set api_url for a local or proxied Bot API endpoint. If your team runs many bots, a small factory that builds a configured bot from a config key pays for itself immediately — it prevents the classic bug where one cron job is using a stale token.

Debugging flow. When a handler "doesn't fire," log the entire raw update first (not a summary), then check handler ordering and your trigger() conditions. Most misses are either a fallback handler registered too early or an update type you didn't anticipate — edited_message, channel_post, and my_chat_member are the usual surprises.

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
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect