Search by

gfn / telegram-php-sdk

JackHusky

A PHP SDK for the Telegram-API.

2.0.0 2026-11-10 10:24 UTC

This package is auto-updated.

Last update: 2026-11-10 10:24:38 UTC


README

An SDK written in PHP to communicate with the Telegram Bot API.

Last supported Bot API Version: 10.4 (2026-11-09)

All useful Information to Telegram Bots can you find on the official Website.

This is only an SDK, not a Bot itself.

Disclaimer

This project and its author are neither associated nor affiliated with Telegram in any way.

Data Protection / GDPR (DSGVO)

Telegram is an external third-party service. Any application built on this SDK transmits data to Telegram's infrastructure whenever it sends or receives messages — this SDK itself does not collect, store, or process personal data beyond what is required to make a single API call.

What may be transmitted, depending on which methods you use: chat and user identifiers, message text and captions, uploaded media (photos, documents, voice notes, etc.) and files you upload with InputFile, contact and location data, HTML widgets with their data, and any other content you pass to a request class. On the incoming side, every webhook payload Telegram sends you may contain personal data about the sender (name, username, language, etc. — see User). Since Bot API 10.4 this also includes TON wallet data (wallet addresses, balances, public keys, NFTs of a TonWalletCard, transfers, signed data of a widget query): blockchain addresses are public and can usually be linked to a person, so treat them as personal data.

You, the developer integrating this SDK, are the data controller for your bot. Using this SDK does not by itself make you GDPR/DSGVO-compliant — you remain responsible for:

  • having a lawful basis (Art. 6 GDPR) for processing the personal data your bot handles,
  • informing end users how their data is processed (transparency, Art. 13/14 GDPR),
  • the fact that Telegram may process and store data outside your own infrastructure, potentially involving a transfer to a third country — review Telegram's Privacy Policy and, where applicable, put a data-processing agreement in place with Telegram,
  • not logging or persisting personal data (message content, user identifiers, bot tokens) beyond what your own lawful basis and retention policy permit,
  • treating widget data (html, public_data, chat_data, the data of a widget query) and TON signatures as untrusted input: the SDK passes them through unchanged and does not verify a signed sign_data; never put them unescaped into HTML or SQL, and never pass a path that comes from incoming data to InputFile::fromPath().

Exception messages of this SDK never contain the bot token or request parameters (message content), see docs/changes/API_10_4.md.

This section is guidance, not legal advice — consult your own data-protection counsel for your specific use case.

License

This project is released under the MIT License.

Requirements

Installation

The easiest way is the installation via Composer: composer require gfn/telegram-php-sdk

Version 2.0.0 contains breaking changes. If you update from 1.x, change the constraint (composer require gfn/telegram-php-sdk:^2.0) and follow the upgrade guide.

Alternatively you can add this GIT-Repository to your composer.json:

{
	"repositories": [
		{
			"type": "git",
			"url": "https://gitlab.german-furs.net/german-furs/telegram-php-sdk.git",
			"no-api": true
		}
	]
}

Usage

First you need to create a bot via BotFather and authorize it. Save the token and hold it save.

All classes have links to the original documentation. If you are not sure how to use the class, check out the documentation.

Using this SDK with AI agents

If you are an AI agent (or a developer directing one) integrating this SDK into a bot, read docs/AGENT_USAGE.md first — it documents the request/response pattern, webhook handling, polymorphic types, and error handling in detail. For the exact, current set of available Types and Request classes, see the local mirror of the official documentation at docs/api-reference/, which is kept in sync with this SDK's implementation (see CLAUDE.md for the governance rules this project follows).

Examples requests outgoing

getMe

With the getMe method, you can request all information about your bot.

$bot_token	= 'xxx';
$get_me		= new \GfnTelegramPhpSdk\Core\Request\GetMe();

try {
	$get_me->execute($bot_token)
	var_dump($get_me->getResponse());
} catch (\GfnTelegramPhpSdk\Exception\GfnTelegramPhpSdkException $e) {
	var_dump($e);
}

sendMessage

With the sendMessage method, you can send a message to a user or chat.

$bot_token	= 'xxx';
$chat_id	= 000;

$sm		= new SendMessage();
$sm->chat_id	= $chat_id;
$sm->text	= 'Test Message';

// Example with inline buttons.
// Buttons One & Two are on line 1, Button Three is on line 2.
// Each InlineKeyboardButton needs a purpose, so I used the callback_data.
$button_one_data	= [
	'text'		=> 'Button One',
	'callback_data'	=> 'callback_one'
];
$button_two_data	= [
	'text'		=> 'Button Two',
	'callback_data'	=> 'callback_two'
];
$button_three_data	= [
	'text'		=> 'Button Three',
	'callback_data'	=> 'callback_three'
];

$inline_keyboard_markup	= new \GfnTelegramPhpSdk\Core\Type\InlineKeyboardMarkup(['inline_keyboard' => [[$button_one_data, $button_two_data],[$button_three_data]]]);
$sm->reply_markup	= $inline_keyboard_markup;

try {
	$sm->execute($bot_token)
	var_dump($sm->getResponse());
} catch (\GfnTelegramPhpSdk\Exception\GfnTelegramPhpSdkException $e) {
	var_dump($e);
}

File Upload

To send files to users or chats, pass an InputFile (a new file), a file_id (a file which already exists on the Telegram servers) or an HTTP URL to the respective methods. The request is sent as multipart/form-data automatically as soon as a file is part of it.

An InputFile is created with InputFile::fromPath(), InputFile::fromContents() or InputFile::fromResource(). Never pass a path which comes from untrusted input (e.g. from a webhook payload).

Sending Documents

Use sendDocument to send general files like ZIP archives, PDFs, etc.

use GfnTelegramPhpSdk\Core\Type\InputFile;

$bot_token	= 'xxx';
$chat_id	= 000;

// Send a ZIP file from local file system
$sd		= new \GfnTelegramPhpSdk\Core\Request\SendDocument();
$sd->chat_id	= $chat_id;
$sd->document	= InputFile::fromPath('/path/to/archive.zip');
$sd->caption	= 'Here is your archive';

try {
	$sd->execute($bot_token);
	var_dump($sd->getResponse());
} catch (\GfnTelegramPhpSdk\Exception\GfnTelegramPhpSdkException $e) {
	var_dump($e);
}

// Alternatively, use a file_id from a previously uploaded file
$sd->document = 'BQACAgIAAxkBAAI...'; // file_id from previous upload
Sending Photos

Use sendPhoto to send images.

use GfnTelegramPhpSdk\Core\Type\InputFile;

$bot_token	= 'xxx';
$chat_id	= 000;

// Send a photo from local file system
$sp		= new \GfnTelegramPhpSdk\Core\Request\SendPhoto();
$sp->chat_id	= $chat_id;
$sp->photo	= InputFile::fromPath('/path/to/photo.jpg');
$sp->caption	= 'This is a photo';

try {
	$sp->execute($bot_token);
	var_dump($sp->getResponse());
} catch (\GfnTelegramPhpSdk\Exception\GfnTelegramPhpSdkException $e) {
	var_dump($e);
}

// Alternatively, use a file_id from a previously uploaded file
$sp->photo = 'AgACAgIAAxkBAAI...'; // file_id from previous upload
Sending a media group

Files inside of nested objects are uploaded automatically, the SDK sends them as attach://<name>.

use GfnTelegramPhpSdk\Core\Type\InputFile;
use GfnTelegramPhpSdk\Core\Type\InputMediaPhoto;

$smg		= new \GfnTelegramPhpSdk\Core\Request\SendMediaGroup();
$smg->chat_id	= $chat_id;
$smg->media	= [
	new InputMediaPhoto(InputFile::fromPath('/path/to/one.jpg')),
	new InputMediaPhoto(InputFile::fromPath('/path/to/two.jpg')),
];

Widgets

Bot API 10.4 adds HTML widgets. Send one with sendWidget and answer its queries (an update of the kind widget_query, the Type WidgetQuery) with answerWidgetQuery within 15 seconds.

$sw		= new \GfnTelegramPhpSdk\Core\Request\SendWidget();
$sw->chat_id	= $chat_id;
$sw->html	= '<button onclick="...">Press</button>';
$sw->allow_queries	= true;

$sw->execute($bot_token);

// Later, in the webhook handler for a WidgetQuery $widget_query:
$awq			= new \GfnTelegramPhpSdk\Core\Request\AnswerWidgetQuery();
$awq->widget_query_id	= $widget_query->id;
$awq->data		= 'answer';

$awq->execute($bot_token);

The data of a widget and of its queries is untrusted input, see the data protection section.

Ephemeral reply to a callback query

A message which only the user who pressed an inline button sees. Without receiver_user_id the parameter chat_id is not needed:

$sm		= new \GfnTelegramPhpSdk\Core\Request\SendMessage();
$sm->text	= 'Only for you';
$sm->ephemeral_message_parameters	= new \GfnTelegramPhpSdk\Core\Type\EphemeralMessageParameters([
	'callback_query_id'	=> $callback_query->id,
]);

$sm->execute($bot_token);

Rich Messages

Bot API 10.1 introduces Rich Messages — highly structured, formatted content (headings, lists, tables, block quotes, media blocks, LaTeX, maps, collages and more).

To send a rich message, build an InputRichMessage from Rich Markdown or Rich HTML (exactly one of both) and pass it to sendRichMessage.

$bot_token	= 'xxx';
$chat_id	= 000;

// Build the content from Rich Markdown (or use the html: parameter instead)
$input_rich_message	= new \GfnTelegramPhpSdk\Core\Type\InputRichMessage(
	markdown: "# Title\n\nSome **bold** and *italic* text with a [link](https://telegram.org)."
);

$srm			= new \GfnTelegramPhpSdk\Core\Request\SendRichMessage();
$srm->chat_id		= $chat_id;
$srm->rich_message	= $input_rich_message;

try {
	$srm->execute($bot_token);
	var_dump($srm->getResponse());
} catch (\GfnTelegramPhpSdk\Exception\GfnTelegramPhpSdkException $e) {
	var_dump($e);
}

You can stream a partial message while it is being generated (e.g. for AI-generated replies) with sendRichMessageDraft. The draft is ephemeral (a 30-second preview); call sendRichMessage with the complete message afterwards to persist it.

A received rich message is available on the Message object via $message->rich_message (a RichMessage holding an array of RichBlock elements).

Validation before sending

The Bot API documentation states requirements for many values: the length of a text, the range of a number, the number of list elements, the allowed values, parameters which are required if another one is missing, fields which exclude each other. This SDK checks them before anything is sent, so you get the error at once, with the property and the requirement, and not only a rejection by Telegram. The check runs automatically in execute(); call validate() to check earlier. Objects inside of the request (keyboards, media, reply parameters, ...) are checked as well.

use GfnTelegramPhpSdk\Exception\GfnTelegramPhpSdkValidationException;

$sm		= new \GfnTelegramPhpSdk\Core\Request\SendMessage();
$sm->chat_id	= $chat_id;
$sm->text	= $very_long_text;

try {
	$sm->validate();	// or just execute(), it validates first
} catch (GfnTelegramPhpSdkValidationException $e) {
	var_dump($e->violations);	// e.g. ["SendMessage.text must have 1 to 4096 characters after entities parsing, it has 5000"]
}

The messages never contain the checked value (it may be message content). The requirements are declared as attributes (Core/Validation/: Length, Range, Count, Choice, RequiredUnless, ExactlyOne, AtMostOne) and derived from the wording of the online documentation; the contract test makes sure that they are exactly the documented ones. What the documentation states in prose which can not be checked without context (e.g. "required if the message is sent to a direct messages chat", the limits of a rich message as a whole) is still checked by Telegram only.

"Characters after entities parsing" is counted without the markup of the parse_mode (HTML or MarkdownV2); with entities the text is taken as it is.

Formatting and length helpers

Helper classes in GfnTelegramPhpSdk\Core\Formatting, all following the rules of the documentation section Formatting options:

use GfnTelegramPhpSdk\Core\Formatting\Html;
use GfnTelegramPhpSdk\Core\Formatting\MarkdownV2;
use GfnTelegramPhpSdk\Core\Formatting\PlainText;
use GfnTelegramPhpSdk\Core\Formatting\Text;

// HTML: escape() replaces <, > and &. The wrapping methods expect HTML (so they nest), code() and pre() plain text.
$sm->text		= Html::bold('Hello ' . Html::escape($user_name)) . ' ' . Html::link('docs', 'https://core.telegram.org') . Html::pre($code, 'php');
$sm->parse_mode		= Html::PARSE_MODE;

// MarkdownV2: escape() escapes every character which has a meaning; escapeCode() and escapeUrl() for the special places.
$sm->text		= MarkdownV2::bold('Hello ' . MarkdownV2::escape($user_name)) . ' ' . MarkdownV2::spoiler('secret');
$sm->parse_mode		= MarkdownV2::PARSE_MODE;

// Lengths in the units of the documentation: characters, bytes, UTF-16 code units (entity offsets).
Text::length($text, Text::CHARACTERS);
Text::fits($caption, 1024);
$short		= Text::truncate($caption, 1024, Text::CHARACTERS, '…');	// never cuts a character in half
PlainText::of($html, Html::PARSE_MODE);	// the text without markup, as the Bot API counts it

There are builders for bold, italic, underline, strikethrough, spoiler, code, pre, links, user mentions, custom emoji, date-time entities and block quotations in both classes. The legacy parse mode Markdown is not supported, the Bot API recommends MarkdownV2. Rich messages (Rich Markdown / Rich HTML) are not covered by a builder.

Examples requests incoming

All incoming traffic is checked. If the request does not come from the Telegram network, it will be ignored.

It's important to send a reply in all possible cases, even if Telegram doesn't process it. It's only correct if you receive a response to your request.

If you do not send code 200 as a reply, Telegram will consider this a failed communication attempt and try to send the message again later.

// Cancel the request if it does not come from the Telegram network or is invalid
try {
	if (!Update::isValidRequest()) {
		header('Content-Type: application/json');
		echo '{"success":false,"status":403}';

		exit;
	}
} catch (\GfnTelegramPhpSdk\Exception\GfnTelegramPhpSdkException) {
	header('Content-Type: application/json');
	echo '{"success":false,"status":400}';

	exit;
}

$request_class_name	= Update::getRequestType();
$type_name		= Update::getRequestName();

// Unknown type of request
if (empty($request_class_name) || empty($type_name)) {
	header('Content-Type: application/json');
	echo '{"success":false,"status":404}';

	exit;
}

try {
	$request_class	= new $request_class_name(Update::getRequestDecoded()[$type_name]);

	if ($request_class instanceof GfnTelegramPhpSdk\Core\Type\BotSubscriptionUpdated) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\BusinessConnection) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\BusinessMessagesDeleted) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\CallbackQuery) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\ChatBoostRemoved) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\ChatBoostUpdated) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\ChatJoinRequest) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\ChatMemberUpdated) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\ChosenInlineResult) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\InlineQuery) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\ManagedBotUpdated) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\Message) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\MessageGenerationStopped) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\MessageReactionCountUpdated) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\MessageReactionUpdated) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\PaidMediaPurchased) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\Poll) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\PollAnswer) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Payments\PreCheckoutQuery) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\SharedWidgetQuery) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Payments\ShippingQuery) {
		// do code
	} elseif ($request_class instanceof GfnTelegramPhpSdk\Core\Type\WidgetQuery) {
		// do code
	} else {
		header('Content-Type: application/json');
		echo '{"success":false,"status":404}';

		exit;
	}
} catch (\GfnTelegramPhpSdk\Exception\GfnTelegramPhpSdkException $e) {
	header('Content-Type: application/json');
	echo '{"success":false,"status":400}';

	exit;
}

// Request was successful
header('Content-Type: application/json');
echo '{"success":true,"status":200}';

Long polling instead of a webhook

If your bot can not receive webhooks, fetch the updates with getUpdates. Every entry holds the update_id, the name of the update, the Type object and the raw update (the Type object is null for an update kind this SDK does not know yet):

$gu		= new \GfnTelegramPhpSdk\Core\Request\GetUpdates();
$gu->offset	= $next_offset;
$gu->timeout	= 30;

$gu->execute($bot_token);

foreach ($gu->getResponse() as $entry) {
	$next_offset	= $entry['update_id'] + 1;

	if ($entry['update'] instanceof \GfnTelegramPhpSdk\Core\Type\Message) {
		// do code
	}
}