← Home

#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: stickersendSticker, animationsendAnimation, video_notesendVideoNote, imagesendPhoto, videosendVideo, audiosendAudio. 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.