natilosir / telegram-bot-sdk
A powerful PHP SDK for building Telegram bots with Laravel integration, queue support, and easy setup.
Package info
github.com/natilosir/Telegram-Bot-SDK
Language:HTML
Type:project
pkg:composer/natilosir/telegram-bot-sdk
Requires
- php: >=8.0
- ext-curl: *
- ext-json: *
- ext-pdo: *
- natilosir/bot: *
- natilosir/verta: *
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
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-sdkis the ready-to-run bot project / starter application.natilosir/botis the underlying reusable PHP bot library installed invendor/.
Table of Contents
- Why this SDK?
- Features
- Requirements
- Installation
- Project Structure
- Quick Start
- Configuration
- Bootstrap and Application Paths
- Telegram and Bale Drivers
- Webhooks
- Bot Requests and PendingCall
- Most-Used Bot Methods
- Keyboards
- Routing
- Request Object
- Conversation State Management
- Database and Eloquent Models
- HTTP Client
- Logging and Debugging
- Bale Bot API Support
- Telegram Bot API Coverage
- Raw API Calls
- Custom Drivers
- Browser Code Editor
- Security Best Practices
- Troubleshooting
- FAQ
- Contributing
- License
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_tokenvalidation support - Bale webhook support
- Separate webhook path per driver
- Unified
Requestclass - 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
UserEloquent model - Illuminate Database / Eloquent ORM
- Multiple database connection configuration
- Illuminate-based HTTP client
- Rich HTML logger
lg(),log(),dd()anddad()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
curlextension - PHP
jsonextension - PHP
pdoextension - A publicly accessible HTTPS URL for Telegram webhooks
- A database if you use
Stateor 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 (
telegramorbale) - Telegram/Bale token
- database connection information
It then creates config.php.
If
config.phpalready 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:
- User sends
/profile - Bot asks for phone number
- SDK stores state
phoneNumber - The next unmatched user input is handled by the
phoneNumberstate action - State can then be cleared
State requires a user record
State currently uses:
app\Models\User
with at least:
user_idstate
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.htmlpublicly.
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 + Ssave 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
- Never commit bot tokens. Load tokens from environment variables or a protected secrets system.
- Use Telegram webhook
secret_token. Reject requests that cannot be matched to the expected webhook/secret. - Use HTTPS for public Telegram webhooks.
- Protect
config.php. It may contain database credentials and tokens. - Block
log.htmlin production. Debug logs can contain sensitive payloads. - Disable/remove
editor.phpin production. - Do not expose
vendor/as browsable content. Configure your web server to deny directory listing and direct access where appropriate. - Validate user input. Routing simplifies dispatch; it does not replace authorization or validation.
- Validate uploaded files. Check MIME type, size, extension and storage path before trusting user uploads.
- Rate-limit internal HTTP routes. If you expose routes such as
sendMessageto other applications, authenticate those requests. - Use least-privilege database accounts.
- 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/telegramtoindex.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_datamatches 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:
userstable exists- current user has a row
user_idis populatedstatecolumn is nullable/writable- database configuration is valid
Router/state.phpexists- the state name in
Route::state()matchesState::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