Search by

natilosir / telegram-bot-sdk

natilosir

A powerful PHP SDK for building Telegram bots with Laravel integration, queue support, and easy setup.

Package info

github.com/natilosir/Telegram-Bot-SDK

Homepage

Issues

Documentation

Language:HTML

Type:project

pkg:composer/natilosir/telegram-bot-sdk

Statistics

Installs: 34

Dependents: 0

Suggesters: 0

Stars: 2

1.4.3 2026-09-24 07:56 UTC

This package is auto-updated.

Last update: 2026-09-24 15:31:36 UTC


README

Latest Stable Version Total Downloads PHP Version License Telegram Bot API Bale Bot API

A modern PHP Telegram Bot SDK with a Laravel-like developer experience, multi-driver Telegram + Bale Bot API support, webhooks, routing, conversation state management, inline/reply keyboards, file uploads, Eloquent ORM, an Illuminate-based HTTP client, HTML logging, and a low-level API escape hatch for newly released Bot API methods.

The current Telegram method catalog is synchronized with Telegram Bot API 10.3 and includes recent Bot API capabilities such as rich messages, ephemeral messages, managed bots, guest queries, business features, payments, gifts, forums, reactions, stickers, inline mode, and more.

Package model

  • natilosir/telegram-bot-sdk is the ready-to-run bot project / starter application.
  • natilosir/bot is the underlying reusable PHP bot library installed in vendor/.

Table of Contents

Why this SDK?

Telegram's Bot API is intentionally HTTP-based. That makes it easy to call individual endpoints, but real applications usually need more than raw HTTP requests: routing incoming updates, switching between bot platforms, parsing webhook payloads, managing conversation state, persisting users, uploading files, logging failures, and keeping application code organized.

Telegram Bot SDK provides those application-level building blocks while keeping the Telegram API accessible.

use natilosir\bot\bot;
use natilosir\bot\Request;
use natilosir\bot\Route;

Route::add('/start', function (Request $request) {
    return bot::sendMessage(
        $request->chatID,
        'Hello from Telegram Bot SDK 👋'
    );
});

You can start with the convenient helpers and still fall back to any Bot API method when you need full control:

bot::telegram()->api('sendMessage', [
    'chat_id' => 123456789,
    'text'    => 'Low-level API call',
]);

Features

Bot platforms

  • Telegram Bot API 10.3
  • Bale Bot API
  • Driver-based architecture
  • Runtime driver switching
  • Automatic incoming webhook driver resolution
  • Custom driver extension API

Application architecture

  • Laravel-like project structure
  • Controller-based routing
  • Exact text routes
  • Multiple aliases for one route
  • Regex routes
  • Default/fallback routes
  • Callable and invokable controllers
  • Automatic route dispatch at the end of the request
  • Persian/Arabic character normalization for route matching

Telegram/Bale messaging

  • Text messages
  • Photo, video, audio, voice, document, animation and sticker sending
  • Local file upload
  • URL-based media
  • Message forwarding and copying
  • Message deletion and editing
  • Chat actions such as typing
  • Inline keyboards
  • Reply keyboards
  • Callback query answers and alerts
  • Polls, locations, contacts, venues and media groups
  • Telegram payments and Stars APIs
  • Telegram business APIs
  • Telegram forum/topic APIs
  • Telegram inline mode
  • Telegram gifts, games and sticker APIs
  • New Bot API methods through raw API calls

Webhooks and request parsing

  • Telegram webhook support
  • Telegram secret_token validation support
  • Bale webhook support
  • Separate webhook path per driver
  • Unified Request class
  • Callback query parsing
  • Inline query parsing
  • Payments/shipping updates
  • Poll and poll answer updates
  • Chat member and join request updates
  • Business, guest, managed-bot and subscription update support
  • Non-bot JSON/multipart requests using the same routing layer

Persistence and utilities

  • Conversation state persisted through a User Eloquent model
  • Illuminate Database / Eloquent ORM
  • Multiple database connection configuration
  • Illuminate-based HTTP client
  • Rich HTML logger
  • lg(), log(), dd() and dad() helpers
  • Application container and path helpers
  • Optional browser-based development editor
  • PhpStorm metadata for driver-aware autocomplete

Requirements

For the starter project:

  • PHP 8.0 or newer
  • Composer
  • PHP curl extension
  • PHP json extension
  • PHP pdo extension
  • A publicly accessible HTTPS URL for Telegram webhooks
  • A database if you use State or Eloquent models

Check the required PHP extensions:

php -m | grep -E "curl|json|PDO"

Installation

Option 1 — Create a complete bot project

This is the recommended path if you are starting a new bot.

composer create natilosir/telegram-bot-sdk

The starter project depends on natilosir/bot. Composer runs the package installer through post-autoload-dump.

On the first installation, the interactive installer can ask for:

  • timezone
  • default bot driver (telegram or bale)
  • Telegram/Bale token
  • database connection information

It then creates config.php.

If config.php already exists, the installer leaves it unchanged.

Option 2 — Install only the reusable bot library

If you already have a PHP project and only want the underlying SDK:

composer require natilosir/bot

Then bootstrap the package yourself using natilosir\bot\Bootstrap.

After changing Composer autoloaded classes

composer dump-autoload

Project Structure

A typical starter project looks like this:

my-bot/
├── app/
│   ├── Controllers/
│   │   └── StartController.php
│   ├── Helper/
│   │   └── Menu.php
│   ├── Models/
│   │   └── User.php
│   └── State/
│       └── StartState.php
├── Router/
│   ├── route.php
│   └── state.php
├── vendor/
│   └── natilosir/
│       └── bot/
├── config.php
├── index.php
├── editor.php
├── log.html
├── composer.json
└── .htaccess

Important files

File Purpose
index.php Application entry point and webhook target
config.php Bot drivers, webhook settings, timezone and database configuration
Router/route.php Main command/text/callback routes
Router/state.php Conversation-state handlers
app/Controllers/ Request handlers
app/Models/ Eloquent models
app/State/ State-specific handlers
log.html Development/debug log output
editor.php Optional browser editor for development only

Quick Start

1. Configure your bot

Create or edit config.php:

<?php

return [
    'timezone' => 'Asia/Tehran',
    'locale'   => 'fa',
    'calendar' => 'jalali',

    'bot' => [
        'default' => 'telegram',

        'drivers' => [
            'telegram' => [
                'token'    => getenv('TELEGRAM_BOT_TOKEN') ?: '',
                'base_url' => 'https://api.telegram.org',

                'webhook' => [
                    'url'          => 'https://bot.example.com/webhook/telegram',
                    'secret_token' => getenv('TELEGRAM_WEBHOOK_SECRET') ?: '',
                ],
            ],

            'bale' => [
                'token'    => getenv('BALE_BOT_TOKEN') ?: '',
                'base_url' => 'https://tapi.bale.ai',

                'webhook' => [
                    'url' => 'https://bot.example.com/webhook/bale',
                ],
            ],
        ],
    ],

    'database' => [
        'default' => 'mysql',

        'connections' => [
            'mysql' => [
                'driver'    => 'mysql',
                'host'      => '127.0.0.1',
                'port'      => 3306,
                'database'  => 'telegram_bot',
                'user'      => 'root',
                'password'  => '',
                'charset'   => 'utf8mb4',
                'collation' => 'utf8mb4_unicode_ci',
                'prefix'    => '',
                'strict'    => true,
            ],
        ],
    ],
];

Do not commit production bot tokens or database passwords.

2. Register a route

Router/route.php:

<?php

use app\Controllers\StartController;
use natilosir\bot\Route;

Route::add(
    ['/start', '🏠 Home', 'Back'],
    [StartController::class, 'hello']
);

3. Create a controller

app/Controllers/StartController.php:

<?php

namespace app\Controllers;

use natilosir\bot\bot;
use natilosir\bot\Request;

class StartController
{
    public function hello(Request $request)
    {
        return bot::sendMessage(
            $request->chatID,
            "Welcome {$request->firstName} 👋"
        );
    }
}

4. Set the Telegram webhook

You can use the SDK:

use natilosir\bot\bot;

$result = bot::telegram()
    ->setWebhook('https://bot.example.com/webhook/telegram');

Or let setWebhook() use the configured URL:

$result = bot::telegram()->setWebhook();

Before using the configured value, make sure bot.drivers.telegram.webhook.url is an absolute public HTTPS URL, not only /webhook/telegram.

5. Verify the bot

$me = bot::telegram()->getMe();

lg($me);

Configuration

Application options

return [
    'timezone' => 'Asia/Tehran',
    'locale'   => 'fa',
    'calendar' => 'jalali',
];

Bootstrap applies the configured timezone during application initialization.

Bot driver configuration

'bot' => [
    'default' => 'telegram',

    'drivers' => [
        'telegram' => [
            'token'    => 'YOUR_TELEGRAM_TOKEN',
            'base_url' => 'https://api.telegram.org',
            'webhook'  => [
                'url'          => 'https://example.com/webhook/telegram',
                'secret_token' => 'YOUR_RANDOM_SECRET',
            ],
        ],

        'bale' => [
            'token'    => 'YOUR_BALE_TOKEN',
            'base_url' => 'https://tapi.bale.ai',
            'webhook'  => [
                'url' => 'https://example.com/webhook/bale',
            ],
        ],
    ],
],

bot.default controls the default outgoing driver. Incoming bot webhooks are resolved by the webhook resolver using the configured webhook information.

Compatibility token alias

For backward compatibility:

$token = paths()->config('bot.token');

This resolves to:

bot.drivers.<default-driver>.token

The token does not need to be duplicated in configuration.

Database configuration

The recommended structure supports named connections:

'database' => [
    'default' => 'mysql',

    'connections' => [
        'mysql' => [
            'driver'    => 'mysql',
            'host'      => '127.0.0.1',
            'port'      => 3306,
            'database'  => 'bot',
            'user'      => 'root',
            'password'  => '',
            'charset'   => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix'    => '',
            'strict'    => true,
        ],

        'archive' => [
            'driver'   => 'mysql',
            'host'     => '127.0.0.1',
            'port'     => 3306,
            'database' => 'bot_archive',
            'user'     => 'root',
            'password' => '',
        ],
    ],
],

Bootstrap and Application Paths

The project entry point uses Bootstrap:

<?php

use natilosir\bot\Bootstrap;

require __DIR__ . '/vendor/autoload.php';

$paths = [
    'base_path'    => __DIR__,
    'app_path'     => __DIR__ . '/app',
    'route_path'   => __DIR__ . '/Router',
    'config_path'  => __DIR__ . '/config.php',
    'storage_path' => __DIR__ . '/storage',
    'log_path'     => __DIR__ . '/log.html',
];

$app = new Bootstrap($paths);

Bootstrap initializes:

  • configuration
  • application paths
  • Illuminate container
  • bot services and driver manager
  • timezone
  • route file loading

Bootstrap helpers

$app->basePath();
$app->appPath();
$app->routePath();
$app->configPath();
$app->storagePath();
$app->logPath();

$app->config('timezone');
$app->make(SomeClass::class);

$container = app();
$service   = app(SomeClass::class);

Global path helper

paths()->base;
paths()->app;
paths()->route;
paths()->config;
paths()->storage;
paths()->log;

Append a path:

$file = paths()->route('state.php');
$model = paths()->app('Models/User.php');

Read configuration:

$timezone = paths()->config('timezone');
$driver   = paths()->config('bot.default', 'telegram');

Telegram and Bale Drivers

The bot facade gives you three usage styles.

Use the default driver

use natilosir\bot\bot;

bot::sendMessage($chatId, 'Hello');

The default driver comes from:

bot.default

Explicit Telegram driver

Recommended for Telegram-specific APIs:

bot::telegram()->sendMessage($chatId, 'Hello Telegram');

Explicit Bale driver

bot::bale()->sendMessage($chatId, 'Hello Bale');

Select a driver dynamically

bot::useDriver('telegram')
    ->sendMessage($chatId, 'Telegram selected');

Inspect the selected driver:

$name = bot::driverName();
$driver = bot::currentDriver();

Check whether a driver supports a method

if (bot::telegram()->supports('sendRichMessage')) {
    // Use the method.
}

Or through the manager:

if (bot::supports('sendMessage')) {
    // The current driver supports it.
}

Webhooks

The starter project is webhook-oriented.

Apache rewrite

The included .htaccess forwards non-file/non-directory paths to index.php:

RewriteEngine On

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]

This allows URLs such as:

https://bot.example.com/webhook/telegram
https://bot.example.com/webhook/bale

to enter the same application.

Recommended Telegram webhook configuration

'telegram' => [
    'token' => getenv('TELEGRAM_BOT_TOKEN') ?: '',

    'webhook' => [
        'url'          => 'https://bot.example.com/webhook/telegram',
        'secret_token' => getenv('TELEGRAM_WEBHOOK_SECRET') ?: '',
    ],
],

Register it:

$result = bot::telegram()->setWebhook();

Or explicitly:

$result = bot::telegram()->setWebhook(
    'https://bot.example.com/webhook/telegram',
    secretToken: 'YOUR_SECRET'
);

Telegram webhook secret

When configured, Telegram sends the secret using the webhook secret header and the Telegram driver can use it while resolving incoming webhook requests.

Use a strong random value and keep it outside source control.

Inspect webhook status

$info = bot::telegram()->getWebhookInfo();

lg($info);

Delete the webhook

bot::telegram()->deleteWebhook();

Drop pending updates:

bot::telegram()->deleteWebhook([
    'drop_pending_updates' => true,
]);

Long polling

The SDK exposes getUpdates() too:

$updates = bot::telegram()->getUpdates(
    offset: null,
    limit: 100,
    timeout: 30
);

The supplied starter application itself is primarily designed around incoming webhooks.

Bot Requests and PendingCall

There are two intentionally different execution styles.

Facade/manager calls return PendingCall

Calls made directly on the top-level bot facade are captured by BotManager:

$call = bot::sendMessage($chatId, 'Hello');

This returns a PendingCall, so you can inspect it, defer it, or explicitly execute it.

Concrete driver calls execute immediately

bot::telegram() and bot::bale() return the concrete driver for IDE/static-analysis friendly access. Calls on those concrete drivers execute the HTTP request immediately:

$result = bot::telegram()->getMe();
$result = bot::bale()->getMe();

Therefore this is not valid:

// Wrong: getMe() has already executed and does not return PendingCall.
bot::telegram()->getMe()->send();

If you want a PendingCall while explicitly selecting a platform, keep the call on BotManager:

$call = bot::useDriver('telegram')
    ->sendMessage($chatId, 'Hello Telegram');

$result = $call->result();

The same distinction applies to low-level calls:

// PendingCall:
bot::api('sendMessage', [
    'chat_id' => $chatId,
    'text'    => 'Hello',
])->send();

// Immediate concrete-driver request:
$result = bot::telegram()->api('sendMessage', [
    'chat_id' => $chatId,
    'text'    => 'Hello',
]);

A pending call can be sent explicitly:

$result = $call->send();

result() is an alias that also executes the call:

$result = $call->result();

Automatic execution

If a pending call is neither sent nor inspected, it automatically executes when it is destroyed:

bot::sendMessage($chatId, 'This is sent automatically.');

For application code where timing matters, explicit ->send() or ->result() is clearer.

Inspect a request before sending

$call = bot::sendMessage($chatId, 'Preview me');

$payload = $call->payload();

The payload includes:

  • driver
  • Bot API method
  • full API URL
  • HTTP method
  • request data

Debug without sending

bot::sendMessage($chatId, 'Do not send')
    ->dump();

dump() marks the pending call as inspected and logs it instead of allowing destructor auto-send.

Terminate after dumping:

bot::sendMessage($chatId, 'Debug request')
    ->dd();

PendingCall helpers

$call->url();
$call->method();
$call->httpMethod();
$call->payload();
$call->send();
$call->result();
$call->dump();
$call->dd();

Property/array access on a pending call executes the request and proxies the result.

Most-Used Bot Methods

You do not need to memorize the entire Telegram Bot API. This section focuses on the methods most applications use.

sendMessage()

Send an HTML-formatted Telegram text message:

bot::sendMessage(
    $chatId,
    '<b>Hello!</b> Welcome to the bot.'
);

Reply to a message:

bot::sendMessage(
    $chatId,
    'This is a reply.',
    $request->message_id
);

With markup:

$markup = json_encode([
    'inline_keyboard' => [
        [
            [
                'text' => 'Open website',
                'url'  => 'https://example.com',
            ],
        ],
    ],
], JSON_UNESCAPED_UNICODE);

bot::sendMessage($chatId, 'Choose:', null, $markup);

For newer Telegram parameters, use sendMessageRaw():

bot::telegram()->sendMessageRaw(
    chatID: $chatId,
    text: 'Advanced message',
    parse_mode: 'HTML',
    disable_notification: true
);

You can also use the low-level array form:

bot::telegram()->api('sendMessage', [
    'chat_id' => $chatId,
    'text'    => 'Full Bot API control',
    'parse_mode' => 'HTML',
]);

sendPhoto()

Send a photo:

bot::sendPhoto(
    $chatId,
    '<b>Product photo</b>',
    __DIR__ . '/photo.jpg'
);

Send using an HTTP URL:

bot::sendPhoto(
    $chatId,
    'Remote image',
    'https://example.com/image.jpg'
);

Reply with a photo:

bot::sendPhoto(
    $chatId,
    'Here is the file',
    __DIR__ . '/photo.jpg',
    $request->message_id
);

File descriptors with bot::file()

For raw API calls and advanced multipart requests:

$file = bot::file(
    __DIR__ . '/invoice.pdf',
    'invoice.pdf'
);

bot::telegram()->api('sendDocument', [
    'chat_id'  => $chatId,
    'document' => $file,
]);

sendDocument()

bot::telegram()->sendDocument(
    $chatId,
    __DIR__ . '/manual.pdf',
    'Documentation'
);

sendVideo()

bot::telegram()->sendVideo(
    $chatId,
    __DIR__ . '/video.mp4',
    'Video caption'
);

sendAudio()

bot::telegram()->sendAudio(
    $chatId,
    __DIR__ . '/audio.mp3',
    'Audio caption'
);

sendVoice()

bot::telegram()->sendVoice(
    $chatId,
    __DIR__ . '/voice.ogg',
    'Voice caption'
);

sendSticker()

bot::telegram()->sendSticker(
    $chatId,
    $stickerFileId
);

sendMediaGroup()

For multiple photos/videos, pass Telegram-compatible media data:

bot::telegram()->sendMediaGroup($chatId, [
    [
        'type'  => 'photo',
        'media' => 'https://example.com/1.jpg',
    ],
    [
        'type'  => 'photo',
        'media' => 'https://example.com/2.jpg',
    ],
]);

forwardMessage()

bot::forwardMessage(
    $targetChatId,
    $sourceChatId,
    $messageId
);

copyMessage()

Copy without the forwarded-message header:

bot::copyMessage(
    $targetChatId,
    $sourceChatId,
    $messageId
);

deleteMessage()

bot::deleteMessage(
    $chatId,
    $messageId
);

Delete multiple Telegram messages:

bot::telegram()->deleteMessages(
    $chatId,
    [$messageId1, $messageId2]
);

sendChatAction()

Show the user that the bot is working:

bot::sendChatAction($chatId, 'typing');

Common Telegram actions include:

typing
upload_photo
record_video
upload_video
record_voice
upload_voice
upload_document
choose_sticker
find_location
record_video_note
upload_video_note

answerCallbackQuery()

bot::answerCallbackQuery(
    $request->query_id,
    'Saved successfully'
);

Show a modal alert:

bot::answerCallbackQuery(
    $request->query_id,
    'Important message',
    true
);

Convenience alias:

bot::alert(
    $request->query_id,
    'Done ✅',
    true
);

getMe()

$botInfo = bot::telegram()->getMe();

getFile()

$file = bot::telegram()->getFile($fileId);

The Telegram driver also knows how to build its platform file URL from a Telegram file path.

setMyCommands()

bot::telegram()->setMyCommands([
    [
        'command'     => 'start',
        'description' => 'Start the bot',
    ],
    [
        'command'     => 'help',
        'description' => 'Show help',
    ],
]);

Editing messages

For the flexible edit wrappers, the safest and clearest style is to pass the Telegram Bot API fields as an array:

bot::telegram()->editMessageText([
    'chat_id'    => $chatId,
    'message_id' => $messageId,
    'text'       => '<b>Updated text</b>',
    'parse_mode' => 'HTML',
]);

Edit caption:

bot::telegram()->editMessageCaption([
    'chat_id'    => $chatId,
    'message_id' => $messageId,
    'caption'    => 'Updated caption',
]);

Edit inline keyboard only:

bot::editMessageReplyMarkup(
    $chatId,
    $messageId,
    $replyMarkup
);

Array input is recommended for methods exposed with variadic ...$args, because it maps directly to official Bot API parameter names.

Keyboards

The SDK includes a simple row/column keyboard builder.

Inline keyboard

bot::row([
    bot::column('Account', 'account'),
    bot::column('Website', null, 'https://example.com'),
])->row([
    bot::column('Help', 'help'),
]);

return bot::inline(
    $request->chatID,
    'Choose an option:',
    $request->message_id
);

For inline buttons:

  • second argument of column() is callback data
  • third argument is URL
  • URL takes precedence when supplied

Handle callback data

Route::add('account', function (Request $request) {
    bot::alert($request->query_id, 'Account selected');

    return bot::sendMessage(
        $request->chatID,
        'Your account information...'
    );
});

$request->text is normalized to callback data for callback-query routing.

Reply keyboard

bot::row([
    bot::column('👤 Profile'),
    bot::column('📞 Contact'),
])->row([
    bot::column('❌ Cancel'),
]);

return bot::keyboard(
    $request->chatID,
    'Select an option:',
    $request->message_id
);

Resize and one-time keyboard

bot::keyboard(
    $request->chatID,
    'Choose:',
    $request->message_id,
    copy: false,
    resize: true,
    one_time: true
);

Edit an existing inline keyboard

Build a new keyboard:

bot::row([
    bot::column('✅ Completed', 'done'),
]);

Then:

bot::inline(
    $request->chatID,
    null,
    $request->message_id,
    'edit'
);

For maximum clarity and control, you can call editMessageReplyMarkup() directly with an explicit markup payload.

Force reply

$markup = bot::telegram()->forceReply([
    'input_field_placeholder' => 'Type your name...',
    'selective' => true,
]);

Remove reply keyboard

$markup = bot::telegram()->removeKeyboard([
    'selective' => true,
]);

Clear keyboard builder cache

bot::telegram()->clearCache();

Routing

Routes live in Router/route.php.

Exact text route

Route::add('/start', [StartController::class, 'hello']);

Multiple inputs for one action

Route::add(
    ['/start', 'start', '🏠 Home', 'Back'],
    [StartController::class, 'hello']
);

Callback-data route

Route::add(
    'account',
    [AccountController::class, 'show']
);

Regex route

Route::regex(
    '/^product:(\d+)$/',
    [ProductController::class, 'show']
);

The current router tests whether the normalized input matches the regex. If you need capture values, read the input in the controller and run preg_match() again there.

Closure route

Route::add('/ping', function (Request $request) {
    return bot::sendMessage($request->chatID, 'pong');
});

Invokable controller

Route::add('/help', HelpController::class);

The router calls __invoke(Request $request).

Fallback route

Route::def([FallbackController::class, 'handle']);

JSON response

Useful when the same endpoint receives internal HTTP/API calls:

Route::add('health', function () {
    Route::response([
        'ok' => true,
    ]);
});

Custom status:

Route::response([
    'message' => 'Created',
], 201);

Automatic dispatch

Registering routes schedules route dispatch automatically at request shutdown.

You can also call:

Route::dispatch();

or:

Route::init();

The router guards against dispatching more than once.

Input normalization

Route matching normalizes:

  • leading/trailing whitespace
  • repeated whitespace
  • Arabic ي to Persian ی
  • Arabic ك to Persian ک
  • zero-width joiner/non-joiner characters used in Persian text

This is useful for Persian-language bots where visually identical button text can contain different Unicode forms.

Important note about ->state()

This is valid:

Route::add('/phone', [ProfileController::class, 'askPhone'])
    ->state('phoneNumber');

If Route::add() receives an array of route aliases, the current implementation associates ->state() with the last route registered. If every alias must set the state, register those state routes separately or use Route::registerState() explicitly.

Request Object

Controllers receive:

natilosir\bot\Request

Example:

use natilosir\bot\Request;

class ProfileController
{
    public function show(Request $request)
    {
        $chatId   = $request->chatID;
        $userId   = $request->fromID;
        $text     = $request->text;
        $username = $request->username;
    }
}

Core methods

$request->getInput();
$request->getUpdateType();
$request->getDriverName();
$request->getRawData();
$request->all();

getInput()

Returns the primary routable input.

Depending on the update, that is usually:

  • message text
  • callback data
  • inline query text
$input = $request->getInput();

getUpdateType()

if ($request->getUpdateType() === 'callback_query') {
    // ...
}

getDriverName()

$platform = $request->getDriverName();
// telegram or bale

getRawData()

$raw = $request->getRawData();

all()

Returns the parsed request data exposed by the request object:

$data = $request->all();

Common properties

Property Meaning
$request->updateId Update identifier
$request->updateType Detected update type
$request->driverName Driver/platform name
$request->platform Platform alias
$request->chatID Chat ID
$request->fromID Sender user ID
$request->firstName Sender first name
$request->lastName Sender last name
$request->username Sender username
$request->date Telegram/Bale message timestamp
$request->message_id Message ID
$request->text Routable message/callback input
$request->caption Media caption
$request->entities Message entities
$request->reply_to_message Replied-to message

Media properties

Depending on the update:

$request->photo;
$request->audio;
$request->document;
$request->video;
$request->voice;
$request->contact;
$request->location;
$request->venue;
$request->sticker;
$request->animation;
$request->dice;

Callback query properties

$request->query_id;
$request->callbackData;
$request->chatID;
$request->message_id;

Inline query properties

$request->inline_query_id;
$request->query;
$request->offset;

Shipping and pre-checkout properties

$request->shipping_query_id;
$request->invoice_payload;
$request->shipping_address;

$request->pre_checkout_query_id;
$request->currency;
$request->total_amount;
$request->order_info;

Poll properties

$request->poll_id;
$request->question;
$request->options;
$request->total_voter_count;
$request->is_closed;
$request->is_anonymous;

Chat member properties

$request->old_chat_member;
$request->new_chat_member;
$request->invite_link;

Supported update families

The current request parser recognizes update types including:

message
edited_message
channel_post
edited_channel_post
business_connection
business_message
edited_business_message
deleted_business_messages
guest_message
message_reaction
message_reaction_count
inline_query
chosen_inline_result
callback_query
shipping_query
pre_checkout_query
purchased_paid_media
poll
poll_answer
my_chat_member
chat_member
chat_join_request
chat_boost
removed_chat_boost
managed_bot
subscription
stopped_message_generation

Additional data can still be reached through raw data and dynamic properties.

Using the Bot Endpoint from Another Application

Request also supports non-Telegram/Bale requests.

Send JSON with a top-level route field:

{
  "route": "sendMessage",
  "chat_id": 123456789,
  "text": "Hello from another application"
}

Register the route:

Route::add(
    'sendMessage',
    [ApiController::class, 'sendMessage']
);

Controller:

public function sendMessage(Request $request)
{
    return bot::sendMessage(
        $request->request->chat_id,
        $request->request->text
    );
}

Laravel example:

use Illuminate\Support\Facades\Http;

$response = Http::asJson()
    ->timeout(10)
    ->post('https://bot.example.com/webhook/internal', [
        'route'   => 'sendMessage',
        'chat_id' => 123456789,
        'text'    => 'Hello from Laravel',
    ]);

Nested JSON data

If you intentionally send:

{
  "route": "sendMessage",
  "data": {
    "chat_id": 123456789,
    "text": "Hello"
  }
}

then access it as nested data rather than as top-level properties.

Multipart/file input

The request parser also includes uploaded files from multipart requests and exposes them to site-request handlers.

Conversation State Management

State management lets you build multi-step bot flows.

Example:

  1. User sends /profile
  2. Bot asks for phone number
  3. SDK stores state phoneNumber
  4. The next unmatched user input is handled by the phoneNumber state action
  5. State can then be cleared

State requires a user record

State currently uses:

app\Models\User

with at least:

  • user_id
  • state

The default model can be:

<?php

namespace app\Models;

use natilosir\bot\Model\Model;

class User extends Model
{
    protected $guarded = [];
}

Example users table

The SDK provides Eloquent but does not provide a full migration framework. A minimal MySQL table can look like:

CREATE TABLE users (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT NOT NULL UNIQUE,
    first_name VARCHAR(255) NULL,
    last_name VARCHAR(255) NULL,
    state VARCHAR(255) NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL
);

Create the user before setting state

use app\Models\User;

User::firstOrCreate(
    ['user_id' => $request->fromID],
    [
        'first_name' => $request->firstName,
        'last_name'  => $request->lastName,
    ]
);

Route that starts a state

Route::add(
    '/phone',
    [ProfileController::class, 'askPhone']
)->state('phoneNumber');

Controller:

public function askPhone(Request $request)
{
    return bot::sendMessage(
        $request->chatID,
        'Please send your phone number.'
    );
}

Register state handlers

Router/state.php:

<?php

use app\State\ProfileState;
use natilosir\bot\State;

State::add(
    'phoneNumber',
    [ProfileState::class, 'phoneNumber']
);

State handler:

<?php

namespace app\State;

use natilosir\bot\bot;
use natilosir\bot\Request;
use natilosir\bot\State;

class ProfileState
{
    public function phoneNumber(Request $request)
    {
        $phone = trim($request->getInput());

        // Validate/store the number here.

        State::clear();

        return bot::sendMessage(
            $request->chatID,
            'Phone number saved ✅'
        );
    }
}

Set state manually

State::set('phoneNumber');

Clear state

State::clear();

State fallback

State::def([StateFallbackController::class, 'handle']);

State behavior

When a normal route executes without preserving a state, the router clears the user's state. Routes explicitly associated with a state preserve/set that state before the action executes.

Database and Eloquent Models

natilosir\bot\Model\Model extends Illuminate's Eloquent model.

namespace app\Models;

use natilosir\bot\Model\Model;

class User extends Model
{
    protected $guarded = [];
}

Use familiar Eloquent operations:

$user = User::where('user_id', $request->fromID)->first();

$users = User::whereNotNull('state')->get();

User::firstOrCreate(
    ['user_id' => $request->fromID],
    ['first_name' => $request->firstName]
);

Update:

$user->state = 'waiting_for_name';
$user->save();

Delete:

$user->delete();

Automatic database boot

The base SDK model boots the configured Illuminate Database capsule when the model is created.

Multiple connections

With named database connections in config.php, normal Eloquent connection selection is available in your models:

class ArchiveUser extends Model
{
    protected $connection = 'archive';
}

HTTP Client

The SDK exposes a Laravel/Illuminate-style HTTP facade:

use natilosir\bot\Http;

GET

$response = Http::get(
    'https://api.example.com/users',
    ['page' => 1]
);

POST JSON

$response = Http::asJson()->post(
    'https://api.example.com/users',
    [
        'name' => 'Natilos',
    ]
);

Headers and timeout

$response = Http::withHeaders([
    'Authorization' => 'Bearer TOKEN',
    'Accept'        => 'application/json',
])
->timeout(10)
->get('https://api.example.com/profile');

PUT / PATCH / DELETE

Http::put($url, $data);
Http::patch($url, $data);
Http::delete($url, $data);

Response helpers

$response->status();
$response->body();
$response->json();
$response->object();
$response->headers();
$response->header('Content-Type');

$response->successful();
$response->failed();
$response->clientError();
$response->serverError();

$response->throw();

Debug:

$response->lg();
$response->dd();

Because the HTTP client is built on Illuminate HTTP, fluent request methods available in the installed Illuminate version can also be proxied.

Logging and Debugging

The SDK contains a rich HTML logger.

Correct logger namespace:

use natilosir\bot\log\Log;

Log levels

Log::info('Bot started');
Log::debug('Incoming update', $data);
Log::warning('Slow response');
Log::notice('User state changed');
Log::error('API request failed');

Global debug helpers

lg($request);
log($request);

lg() and log() write debug information.

Dump and stop:

dd($request);

Log and terminate:

dad('Stopping here', $request);

Log output

By default, the starter project uses:

log.html

The logger records readable HTML including:

  • level
  • message/data
  • context
  • source location / trace information
  • exceptions and fatal errors

Production warning: logs may contain tokens, user IDs, payloads, database details or other sensitive information. Do not expose log.html publicly.

Bale Bot API Support

The same project can run a Bale bot.

Configure Bale

'bale' => [
    'token'    => getenv('BALE_BOT_TOKEN') ?: '',
    'base_url' => 'https://tapi.bale.ai',

    'webhook' => [
        'url' => 'https://bot.example.com/webhook/bale',
    ],
],

Send a Bale message

bot::bale()->sendMessage(
    $chatId,
    'Hello from Bale'
);

Bale keyboard

bot::bale()
    ->row([
        bot::bale()->column('Profile', 'profile'),
        bot::bale()->column('Website', null, 'https://example.com'),
    ]);

bot::bale()->inline(
    $chatId,
    'Choose:',
    $messageId
);

For shared application code, prefer the default/current driver where method signatures overlap.

Bale supported API families

The Bale driver includes wrappers for:

  • updates and webhooks
  • messages
  • forwarding/copying
  • photos, audio, documents, videos, animations and voice
  • media groups
  • locations and contacts
  • callbacks
  • chat actions
  • chat administration
  • invite links
  • pin/unpin
  • message editing/deletion
  • stickers
  • payments/invoices
  • transaction inquiry

Bale Business API

Business methods include:

bot::bale()->businessGetMe();

bot::bale()->businessSendMessage([
    // Bale Business API fields
]);

bot::bale()->businessForwardMessage([...]);
bot::bale()->businessSendPhoto([...]);
bot::bale()->businessSendVideo([...]);
bot::bale()->businessSendAudio([...]);
bot::bale()->businessSendDocument([...]);

Telegram Bot API Coverage

The package exposes convenient typed wrappers for frequently used methods and a broader Telegram method catalog for advanced Bot API features.

The following is intentionally a quick reference rather than full method-by-method documentation.

Area Available methods / capabilities
Core getMe, getUpdates, setWebhook, deleteWebhook, getWebhookInfo, logOut, close, getFile, user profile photos/audios
Messages sendMessage, forwardMessage(s), copyMessage(s), deleteMessage(s), sendChatAction, reactions, checklists, dice
Media sendPhoto, sendAudio, sendDocument, sendVideo, sendAnimation, sendVoice, sendVideoNote, sendLivePhoto, sendPaidMedia, sendMediaGroup, sendSticker
Places & people sendLocation, live-location editing/stopping, sendVenue, sendContact
Polls sendPoll, stopPoll, poll update parsing
Callbacks answerCallbackQuery, alert
Editing text, captions, media, reply markup, checklist and live-location editing
Inline mode answerInlineQuery, answerWebAppQuery, prepared inline messages/buttons
Chat admin ban/unban/restrict/promote members, permissions, admin titles/tags, sender-chat controls
Invite links export/create/edit/revoke links, subscription invite links, join request approval/decline
Chat profile title, description, photo, sticker set, pin/unpin messages, leave/get chat, members/admins
Forums create/edit/close/reopen/delete topics, general-topic controls, topic icon stickers
Bot profile commands, name, description, short description, profile photo, menu button, default admin rights
Payments invoices, invoice links, shipping/pre-checkout answers, Stars balance/transactions/refunds/subscriptions
Stickers sticker sets, custom emoji stickers, upload/add/replace/delete stickers, keywords/mask positions/thumbnails
Games sendGame, scores and high scores
Gifts available gifts, send gifts, user/chat gifts, gift upgrades/transfers/conversion, Premium gifts
Verification verify/remove verification for users and chats
Business business connections/messages/account profile/settings/stars/gifts/story operations
Guest mode guest-query answering and guest updates
Managed bots get/replace managed bot token, access settings and managed-bot updates
Ephemeral messages edit/delete ephemeral text/media/caption/reply markup
Rich messages rich-message sending and rich-message draft streaming
Drafts sendMessageDraft, sendRichMessageDraft
Reactions set/remove/delete message reactions
Stories post/repost/edit/delete story APIs through business features

For official field-level parameters, use Telegram's Bot API reference. When a brand-new API method is released before a dedicated wrapper is added, call it through api().

Raw API Calls

Raw API calls are the compatibility escape hatch.

Through the selected/default driver

bot::api('sendMessage', [
    'chat_id' => $chatId,
    'text'    => 'Hello',
])->send();

Telegram explicitly

bot::telegram()->api('sendMessage', [
    'chat_id'    => $chatId,
    'text'       => 'Hello',
    'parse_mode' => 'HTML',
]);

Bale explicitly

bot::bale()->api('sendMessage', [
    'chat_id' => $chatId,
    'text'    => 'Hello Bale',
]);

Call a newly released method

bot::telegram()->api('someFutureMethod', [
    'example' => 'value',
]);

The SDK does not need a convenience wrapper before you can use an API endpoint.

Custom Drivers

The architecture is not limited to Telegram and Bale.

A custom driver implements:

natilosir\bot\Bot\Contracts\BotDriver

A webhook-aware driver also implements:

natilosir\bot\Bot\Contracts\WebhookAwareDriver

Core driver responsibilities include:

  • driver name
  • bot token
  • base API URL
  • method support detection
  • API execution
  • file URL generation
  • request capture for PendingCall

Register/extend a driver through DriverManager:

use natilosir\bot\Bot\Manager\DriverManager;

$manager = app(DriverManager::class);

$manager->extend('my-platform', function () {
    return new MyPlatformDriver(/* ... */);
});

Then:

bot::driver('my-platform')
    ->api('sendMessage', [
        // ...
    ]);

This makes it possible to add another messaging platform without replacing the application routing layer.

Browser Code Editor

The starter project contains editor.php, a Monaco-based browser code editor.

Example:

editor.php?file=app/Controllers/StartController.php

Features include:

  • PHP/JS/HTML/CSS editing
  • browser-based file navigation
  • syntax highlighting
  • Ctrl + S save shortcut
  • desktop/mobile-friendly interface

Development only

Do not expose the editor publicly on a production bot server.

The editor's supporting save/load endpoints can modify project files. In production, one of these approaches is recommended:

  • remove editor.php
  • remove/block the package editor save/load endpoints
  • deny access at the web server level
  • protect the editor behind strong authentication and an IP allowlist
  • keep development tools on a separate environment

The safest production deployment does not make the editor reachable from the public internet.

Security Best Practices

  1. Never commit bot tokens. Load tokens from environment variables or a protected secrets system.
  2. Use Telegram webhook secret_token. Reject requests that cannot be matched to the expected webhook/secret.
  3. Use HTTPS for public Telegram webhooks.
  4. Protect config.php. It may contain database credentials and tokens.
  5. Block log.html in production. Debug logs can contain sensitive payloads.
  6. Disable/remove editor.php in production.
  7. Do not expose vendor/ as browsable content. Configure your web server to deny directory listing and direct access where appropriate.
  8. Validate user input. Routing simplifies dispatch; it does not replace authorization or validation.
  9. Validate uploaded files. Check MIME type, size, extension and storage path before trusting user uploads.
  10. Rate-limit internal HTTP routes. If you expose routes such as sendMessage to other applications, authenticate those requests.
  11. Use least-privilege database accounts.
  12. Keep Composer dependencies updated.

Recommended .gitignore entries for deployments that keep local secrets/logs:

/vendor/
/config.php
/log.html
/.env

If you intentionally version a non-secret config template, keep a config.example.php and generate/copy the real config.php during deployment.

Troubleshooting

Bot does not receive updates

Check:

$info = bot::telegram()->getWebhookInfo();
lg($info);

Verify:

  • webhook URL is public
  • URL uses HTTPS
  • web server routes /webhook/telegram to index.php
  • token is correct
  • Telegram webhook secret matches configuration
  • PHP errors are not terminating the request before routing

Route not found for input

Add a fallback:

Route::def([FallbackController::class, 'handle']);

Log the parsed input:

lg($request->getInput(), $request->getUpdateType());

Callback button does nothing

Check that:

  • callback_data matches a registered route
  • your controller returns/sends a response
  • callback queries are answered when user feedback is expected

Example:

Route::add('confirm', function (Request $request) {
    bot::alert($request->query_id, 'Confirmed ✅');

    return bot::sendMessage($request->chatID, 'Done');
});

State does not persist

Verify:

  • users table exists
  • current user has a row
  • user_id is populated
  • state column is nullable/writable
  • database configuration is valid
  • Router/state.php exists
  • the state name in Route::state() matches State::add()

Media upload fails

Try:

  • an absolute local file path
  • a reachable HTTPS URL
  • bot::file() with a low-level API call
  • checking PHP upload/file permissions and server limits

Database connection fails

Test the configured credentials and ensure the necessary PDO driver is installed, for example:

php -m | grep -i pdo

config.php is not generated

The Composer installer only creates the file when it does not already exist. Remove/rename an intentionally disposable config and run:

composer dump-autoload

Then answer the interactive installer prompts.

Do not delete a production config unless you have a safe backup of its credentials.

Changes to app classes are not detected

composer dump-autoload

FAQ

Is this only for Telegram?

No. The current built-in drivers are Telegram and Bale, and the driver manager can be extended with custom platforms.

Does it require Laravel?

No. It is a standalone PHP project. It uses Illuminate components and a Laravel-like API/style for familiar HTTP, database and container behavior.

Can I use it inside an existing Laravel project?

The reusable library can be installed through Composer, and a Laravel application can also call a standalone bot webhook over HTTP. There is no requirement that the starter project itself run inside Laravel.

Does it support Telegram Bot API 10.3?

The current Telegram method catalog in the package targets Telegram Bot API 10.3.

What happens when Telegram adds a new method?

Use the low-level API immediately:

bot::telegram()->api('newMethodName', [
    // official Telegram parameters
]);

A dedicated wrapper can be added later.

Do I need ->send() after every call?

For top-level facade calls such as bot::sendMessage(...), no. They return a PendingCall, which auto-executes if it is destroyed without being inspected/sent. Explicit ->send() or ->result() is recommended when execution timing or the returned API value matters.

Concrete-driver calls such as bot::telegram()->sendMessage(...) execute immediately and therefore do not need — or return an object for — ->send().

What is the difference between send() and result()?

On a PendingCall, both execute the pending API call and return its result.

How can I see a request without sending it?

bot::sendMessage($chatId, 'Test')->dump();

Can one application serve both Telegram and Bale webhooks?

Yes. Configure separate webhook URLs and let the driver/webhook resolver identify the incoming platform.

Are states stored in memory?

No. The current State implementation uses the app\Models\User Eloquent model and stores the state in the user's database row.

Can I use regex routes?

Yes:

Route::regex('/^order-\d+$/', [OrderController::class, 'show']);

Can I call my bot from another website/API?

Yes. POST JSON with a route field to the bot application and register a matching route.

Recommended Production Layout

For better isolation, point your web server document root at a public directory or explicitly deny direct access to sensitive files.

At minimum, do not publicly serve:

config.php
log.html
composer.json
vendor/ package internals
editor.php

Only the webhook/front-controller entry point and intentionally public assets should be reachable.

IDE / PhpStorm Support

The underlying package contains PhpStorm metadata to improve driver-aware autocomplete.

Prefer explicit typed drivers when your code uses platform-specific methods:

bot::telegram()->sendRichMessage(/* ... */);
bot::bale()->inquireTransaction(/* ... */);

This is clearer to both developers and IDEs than relying exclusively on dynamic calls.

Contributing

Contributions are welcome.

Before opening a pull request:

composer install
composer dump-autoload

When adding a Telegram method:

  • follow the official Bot API parameter names
  • keep the low-level api() path working
  • add/update the method catalog where applicable
  • preserve backward compatibility where practical
  • document new public behavior

For bugs or feature requests, open a GitHub issue with:

  • PHP version
  • SDK/package version
  • bot driver (telegram / bale)
  • minimal reproduction
  • relevant sanitized request/update payload
  • error message or log excerpt with all tokens/secrets removed

Support

  • GitHub Issues: use the repository issue tracker for reproducible bugs and feature requests.
  • Source code: natilosir/Telegram-Bot-SDK
  • Composer project: natilosir/telegram-bot-sdk
  • Core library: natilosir/bot

When sharing logs publicly, always remove:

  • bot tokens
  • webhook secrets
  • database passwords
  • private user data

Disclaimer

Telegram Bot SDK is a third-party open-source project and is not affiliated with, endorsed by, or maintained by Telegram.

Bale-related support is also provided as an independent SDK integration.

License

This project is open-sourced software licensed under the MIT License.

Keywords

PHP Telegram Bot SDK · Telegram Bot API 10.3 · PHP Telegram Bot · Telegram Bot Framework · Telegram Webhook PHP · Telegram Inline Keyboard PHP · Telegram Bot Composer Package · PHP Bot SDK · Bale Bot API · Bale Bot PHP · Telegram Eloquent Bot · Illuminate Telegram Bot · Telegram Bot Routing · Telegram Conversation State · Telegram File Upload PHP