TelegramPHP Documentation

Updates and events

An update is Telegram telling your bot that something happened. Whether they arrive by long polling or by webhook, they reach your code the same way: as events.

Each update is emitted twice - once as Event::UPDATE carrying the whole Update part, and once under its own type carrying just that payload. The second form is what most handlers want:

use Telegram\Events\Event;
use Telegram\Parts\CallbackQuery;
use Telegram\Parts\Message;

$telegram->on(Event::MESSAGE, function (Message $message) {
    // a new message of any kind
});

$telegram->on(Event::CALLBACK_QUERY, function (CallbackQuery $query) {
    // someone pressed an inline button
});

$telegram->on(Event::UPDATE, function (Update $update) {
    // every update, whatever its type
});

Handlers also receive the client as a second argument, so a static closure can still reach it:

$telegram->on(Event::MESSAGE, static function (Message $message, Telegram $telegram) {
    $telegram->sendChatAction($message->chat->id, 'typing');
});

The update types

The constants on Telegram\Events\Event are generated from the Update object itself, so they are exactly the types this Bot API version defines:

Constant Update type Carries
Event::MESSAGE message Message
Event::EDITED_MESSAGE edited_message Message
Event::CHANNEL_POST channel_post Message
Event::EDITED_CHANNEL_POST edited_channel_post Message
Event::BUSINESS_CONNECTION business_connection BusinessConnection
Event::BUSINESS_MESSAGE business_message Message
Event::EDITED_BUSINESS_MESSAGE edited_business_message Message
Event::DELETED_BUSINESS_MESSAGES deleted_business_messages BusinessMessagesDeleted
Event::GUEST_MESSAGE guest_message Message
Event::MESSAGE_REACTION message_reaction MessageReactionUpdated
Event::MESSAGE_REACTION_COUNT message_reaction_count MessageReactionCountUpdated
Event::INLINE_QUERY inline_query InlineQuery
Event::CHOSEN_INLINE_RESULT chosen_inline_result ChosenInlineResult
Event::CALLBACK_QUERY callback_query CallbackQuery
Event::SHIPPING_QUERY shipping_query ShippingQuery
Event::PRE_CHECKOUT_QUERY pre_checkout_query PreCheckoutQuery
Event::PURCHASED_PAID_MEDIA purchased_paid_media PaidMediaPurchased
Event::POLL poll Poll
Event::POLL_ANSWER poll_answer PollAnswer
Event::MY_CHAT_MEMBER my_chat_member ChatMemberUpdated
Event::CHAT_MEMBER chat_member ChatMemberUpdated
Event::CHAT_JOIN_REQUEST chat_join_request ChatJoinRequest
Event::CHAT_BOOST chat_boost ChatBoostUpdated
Event::REMOVED_CHAT_BOOST removed_chat_boost ChatBoostRemoved
Event::MANAGED_BOT managed_bot ManagedBotUpdated
Event::SUBSCRIPTION subscription BotSubscriptionUpdated
Event::STOPPED_MESSAGE_GENERATION stopped_message_generation MessageGenerationStopped

That table is generated from the spec along with the constants themselves, so it lists exactly what this build supports. The same information is available at runtime:

Event::all();            // ['message', 'edited_message', 'channel_post', ...]
Event::PAYLOAD_TYPES;    // ['message' => 'Message', 'callback_query' => 'CallbackQuery', ...]

Choosing what to receive

Telegram withholds some update types unless you ask for them - chat_member and the reaction types among them - and sends everything else whether you use it or not. Asking for only what you handle cuts both traffic and the work of hydrating payloads you throw away:

$telegram = new Telegram([
    'token' => $token,
    'allowed_updates' => [Event::MESSAGE, Event::CALLBACK_QUERY, Event::CHAT_MEMBER],
]);

The same list goes to setWebhook when you are using one.

allowed_updates is remembered by Telegram between calls. Passing null keeps whatever was set last rather than resetting it - pass an explicit list to change it.

Long polling

Polling is the default and needs no public address. Each round calls getUpdates with a timeout, so the connection is held open until Telegram has something to say. Updates are acknowledged by the next round asking for everything past the highest update_id handled, which means an update your handler threw on is not redelivered - it is logged and emitted as Event::ERROR instead.

$telegram = new Telegram([
    'token' => $token,
    'poll_timeout' => 50,          // seconds to hold the connection open
    'poll_limit' => 100,           // updates per round
    'drop_pending_updates' => true, // ignore the backlog from while the bot was down
]);

A failed round backs off - one second, then two, four, up to thirty - rather than hammering a server that is already unhappy, and a retry_after from Telegram is honoured exactly. The poller is reachable while it runs:

$telegram->getPoller()->getOffset();    // the update_id the next round asks from
$telegram->getPoller()->isRunning();

Only one poller may hold a token at a time, and a webhook excludes polling entirely. A second one gets a 409 Conflict - see Handling errors.

Handlers that throw

An exception out of your handler does not stop the bot. It is logged, emitted as Event::ERROR , and the next update is processed:

$telegram->on(Event::ERROR, function (Throwable $e, Telegram $telegram) {
    $logger->error('handler failed', ['exception' => $e]);
});

This applies to exceptions thrown synchronously . A rejected promise inside a handler is not an exception anyone sees unless you handle it - see Handling errors.

Search results