#Telegram Adapter
The Telegram adapter connects your bot to the Telegram Bot API.
#Installation
composer require bootdesk/chat-sdk-adapter-telegram
Requires a PSR-18 HTTP client (guzzlehttp/guzzle, symfony/http-client, etc.) and a PSR-17 factory (nyholm/psr7 bundled).
#Constructor
use BootDesk\ChatSDK\Telegram\TelegramAdapter;
$adapter = new TelegramAdapter(
botToken: '123456:ABC-DEF1234',
httpClient: new \GuzzleHttp\Client,
secretToken: 'my-secret', // optional — verify webhook origin
apiUrl: 'https://api.telegram.org', // optional — default
psrFactory: new \Nyholm\Psr7\Factory\Psr17Factory, // optional
);
#Laravel
Add to config/chat.php:
'telegram' => [
'bot_token' => env('TELEGRAM_BOT_TOKEN'),
'secret_token' => env('TELEGRAM_SECRET_TOKEN'),
],
The ChatServiceProvider auto-binds Psr\Http\Client\ClientInterface.
#Thread ID Format
| Format | Description |
|---|---|
telegram:{chatId} |
Direct message or group chat |
telegram:{chatId}:{messageThreadId} |
Topic within a forum |
$adapter->postMessage('telegram:123456789', 'Hello!');
$adapter->postMessage('telegram:-100123456789:42', 'Topic message');
#Sending Messages
#Text
$adapter->postMessage('telegram:12345', PostableMessage::text('Hello!'));
MarkdownV2 is the default parse mode. HTML is used for cards.
#With Reply-to-Message
use BootDesk\ChatSDK\Core\PostableMessage;
$adapter->postMessage('telegram:12345', new PostableMessage(
content: 'Reply to this',
replyToMessageId: '42',
));
#Attachments (URL-based / file-id-based)
Send via Attachment objects — type determines which Telegram API method is called. The url field accepts either a public URL or a Telegram file_id (from a previously received attachment):
use BootDesk\ChatSDK\Core\Attachment;
use BootDesk\ChatSDK\Core\FileUpload;
// Forward a received sticker by its file_id
$received = $message->attachments[0]; // type: 'sticker'
$adapter->postMessage('telegram:12345', new PostableMessage(
content: '',
attachments: [
new Attachment(
type: 'sticker',
url: $received->fetchMetadata['file_id'],
name: $received->name,
mimeType: $received->mimeType,
),
],
));
// Animation / GIF by public URL
$adapter->postMessage('telegram:12345', new PostableMessage(
content: 'Check this out',
attachments: [
new Attachment(type: 'animation', url: 'https://example.com/dance.mp4', name: 'dance.mp4', mimeType: 'video/mp4'),
],
));
// Forward a video note by its file_id
$receivedVn = $message->attachments[0]; // type: 'video_note'
$adapter->postMessage('telegram:12345', new PostableMessage(
content: '',
attachments: [
new Attachment(
type: 'video_note',
url: $receivedVn->fetchMetadata['file_id'],
),
],
));
Types map to Telegram API: sticker → sendSticker, animation → sendAnimation, video_note → sendVideoNote, image → sendPhoto, video → sendVideo, audio → sendAudio. Unknown types fall back to sendDocument.
Note: sendSticker and sendVideoNote do not support captions. Any text in content is silently dropped for these types.
#Files (binary uploads)
use BootDesk\ChatSDK\Core\FileUpload;
$adapter->postMessage('telegram:12345', new PostableMessage(
content: 'Here is the file:',
files: [FileUpload::fromFilename('/tmp/report.pdf')],
));
Sticker files with mime type image/webp or application/x-tgsticker automatically route to sendSticker.
#Cards (Inline Keyboards)
use BootDesk\ChatSDK\Core\Cards\Card;
use BootDesk\ChatSDK\Core\Cards\Button;
$card = Card::make()
->header('Choose an option:')
->actions([
Button::primary('Confirm', 'confirm_order'),
Button::danger('Cancel', 'cancel_order'),
]);
$adapter->postMessage('telegram:12345', PostableMessage::card($card));
Card buttons become inline keyboards with callback_data or url.
#Custom Reply Keyboards
Pass a ReplyKeyboardMarkup via metadata.reply_markup:
use BootDesk\ChatSDK\Core\PostableMessage;
use BootDesk\ChatSDK\Telegram\Keyboard\KeyboardButton;
use BootDesk\ChatSDK\Telegram\Keyboard\ReplyKeyboardMarkup;
$adapter->postMessage('telegram:12345', new PostableMessage(
content: 'Choose an option:',
metadata: [
'reply_markup' => new ReplyKeyboardMarkup(
keyboard: [
[new KeyboardButton('Help')],
[new KeyboardButton('Settings')],
[new KeyboardButton('Cancel')],
],
resizeKeyboard: true,
oneTimeKeyboard: true,
),
],
));
#Request Location / Contact
use BootDesk\ChatSDK\Telegram\Keyboard\KeyboardButton;
new KeyboardButton('Send Location', requestLocation: true);
new KeyboardButton('Share Contact', requestContact: true);
// Full row:
$row = [
new KeyboardButton('Location', requestLocation: true),
new KeyboardButton('Contact', requestContact: true),
];
#Request Poll
use BootDesk\ChatSDK\Telegram\Keyboard\KeyboardButtonPollType;
new KeyboardButton('Quiz', requestPoll: new KeyboardButtonPollType('quiz'));
new KeyboardButton('Poll', requestPoll: new KeyboardButtonPollType('regular'));
#Inline Keyboards (Standalone)
Not via Card — use InlineKeyboardMarkup directly:
use BootDesk\ChatSDK\Telegram\Keyboard\InlineKeyboardButton;
use BootDesk\ChatSDK\Telegram\Keyboard\InlineKeyboardMarkup;
$adapter->postMessage('telegram:12345', new PostableMessage(
content: 'Pick one:',
metadata: [
'reply_markup' => new InlineKeyboardMarkup(
inlineKeyboard: [
[new InlineKeyboardButton('Open', url: 'https://example.com')],
[new InlineKeyboardButton('Confirm', callbackData: 'confirm')],
],
),
],
));
#Web App Button
Open a Telegram Web App from a button:
use BootDesk\ChatSDK\Telegram\Keyboard\WebAppInfo;
// Inline keyboard
new InlineKeyboardButton('Open App', webApp: new WebAppInfo('https://example.com/app'));
// Reply keyboard
new KeyboardButton('Open App', webApp: new WebAppInfo('https://example.com/app'));
#Force Reply
use BootDesk\ChatSDK\Telegram\Keyboard\ForceReply;
$adapter->postMessage('telegram:12345', new PostableMessage(
content: 'Reply to this message:',
metadata: [
'reply_markup' => new ForceReply(
inputFieldPlaceholder: 'Type your answer...',
selective: true,
),
],
));
#Remove Keyboard
use BootDesk\ChatSDK\Telegram\Keyboard\ReplyKeyboardRemove;
$adapter->postMessage('telegram:12345', new PostableMessage(
content: 'Keyboard hidden.',
metadata: [
'reply_markup' => new ReplyKeyboardRemove,
],
));
#Precedence
Metadata reply_markup takes precedence over the card-generated inline keyboard. If both are present, the metadata value wins.
#Editing Messages
Supports reply_markup to update inline keyboards:
use BootDesk\ChatSDK\Telegram\Keyboard\InlineKeyboardButton;
use BootDesk\ChatSDK\Telegram\Keyboard\InlineKeyboardMarkup;
$adapter->editMessage('telegram:12345', '100', new PostableMessage(
content: 'Updated text',
metadata: [
'reply_markup' => new InlineKeyboardMarkup(
inlineKeyboard: [
[new InlineKeyboardButton('Done', callbackData: 'done')],
],
),
],
));
#Thread Info & Editing
Fetch thread details (forum topic or chat info):
use BootDesk\ChatSDK\Core\ThreadInfo;
// Forum topic — calls getForumTopic API
$info = $adapter->fetchThread('telegram:-100123:42');
echo $info->title; // "General Discussion"
echo $info->iconCustomEmojiId; // "emoji123"
// Regular chat — calls getChat API
$info = $adapter->fetchThread('telegram:-100456');
echo $info->title; // "Project Chat"
echo $info->topic; // "For project discussions"
Edit thread properties with ThreadInfo::withParameters():
// Rename a forum topic
$adapter->editThread('telegram:-100123:42', new ThreadInfo(
id: 'telegram:-100123:42',
channelId: '-100123',
title: 'Renamed Topic',
));
// Or use withParameters from existing info
$info = $adapter->fetchThread('telegram:-100123:42');
$adapter->editThread($info->id, $info->withParameters([
'title' => 'Updated Name',
'iconCustomEmojiId' => null,
]));
Supported operations per context:
| Context | Update method | Fields |
|---|---|---|
| Forum topic | editForumTopic |
title, iconCustomEmojiId |
| Forum topic | closeForumTopic / reopenForumTopic |
isArchived |
| Group/supergroup | setChatTitle |
title |
| Group/supergroup | setChatDescription |
topic |
#Deleting Messages
$adapter->deleteMessage('telegram:12345', '100');
#Keyboards Reference
All keyboard value objects implement ReplyMarkup interface.
| Value object | Fields |
|---|---|
KeyboardButton |
text, requestContact (?bool), requestLocation (?bool), requestPoll (?KeyboardButtonPollType), webApp (?WebAppInfo) |
InlineKeyboardButton |
text, callbackData (?string), url (?string), webApp (?WebAppInfo) |
WebAppInfo |
url (string) |
ReplyKeyboardMarkup |
keyboard (KeyboardButton[][]), resizeKeyboard, oneTimeKeyboard, inputFieldPlaceholder, selective |
InlineKeyboardMarkup |
inlineKeyboard (InlineKeyboardButton[][]) |
ForceReply |
inputFieldPlaceholder, selective |
ReplyKeyboardRemove |
selective |
KeyboardButtonPollType |
type — constants QUIZ, REGULAR |
Raw arrays are also accepted in metadata.reply_markup for power users.
#Feature Matrix
| Feature | Supported |
|---|---|
| Post messages | ✓ |
| Edit messages | ✓ |
| Delete messages | ✓ |
| Reactions | ✓ |
| Reply keyboards | ✓ |
| Inline keyboards | ✓ |
| Force reply | ✓ |
| Reply-to-message | ✓ |
| File uploads | ✓ |
| URL attachments | ✓ |
| Stickers (in/out) | ✓ |
| Animations (in/out) | ✓ |
| Video notes (in/out) | ✓ |
| Typing indicator | ✓ |
| Streaming | ✓ |
| Bot commands | ✓ |
| Group chats | ✓ |
| Topic forums | ✓ |
| Thread info & edit | ✓ |
#Webhook
Telegram sends updates via POST. Verify requests using the secret_token set during webhook registration.
// Laravel route
Route::match(['get', 'post'], '/chats/telegram', [WebhookController::class, 'handle']);
For raw usage, see Creating an Adapter.