Telegram Bots Book
Telegram.Bot is the most popular .NET client for Telegram Bot API, allowing developers to build bots for Telegram messaging app.
This book covers all you need to know to create a chatbot in .NET, with many concrete examples written in C#.
Begin with our Quickstart, or choose from the Table Of Content (left/top), and don't miss our useful Frequently Asked Questions.
🧩 Installation
Install the package from Nuget using Nuget package manager (in your IDE or command line).
🎉 The problem with Nuget.org seems resolved. Our latest library version 22.* is now available on it!
Make sure to follow the Migration Guide for v22.* if you have existing bot code.
🪄 More examples
This book is filled with ready-to-use snippets of code, but you can also find full project examples at our Telegram.Bot.Examples Github repository, featuring:
- Simple Console apps (long polling)
- Webhook ASP.NET example (with Controllers or Minimal APIs)
- Full-featured advanced solution
- Serverless Functions implementations
🔗 More useful links
Visit our | URL |
---|---|
Nuget feed | https://www.nuget.org/packages/Telegram.Bot |
Github repo | https://github.com/TelegramBots/Telegram.Bot |
Examples repo | https://github.com/TelegramBots/Telegram.Bot.Examples |
Telegram news channel | https://t.me/tgbots_dotnet |
Telegram support group | https://t.me/joinchat/B35YY0QbLfd034CFnvCtCA |
Team page | https://github.com/orgs/TelegramBots/people |
Quickstart
Bot Father
Before you start, you need to talk to @BotFather on Telegram. Create a new bot, acquire the bot token and get back here.
Bot token is a key that required to authorize the bot and send requests to the Bot API. Keep your token secure and store it safely, it can be used to control your bot. It should look like this:
1234567:4TT8bAc8GHUspu3ERYn-KGcvsvGB9u_n4ddy
Hello World
Now that you have a bot, it's time to bring it to life!
note
We recommend a recent .NET version like .NET 8, but we also support older .NET Framework (4.6.1+), .NET Core (2.0+) or .NET (5.0+)
Create a new console project for your bot and add a reference to Telegram.Bot
package:
dotnet new console
dotnet add package Telegram.Bot
The code below fetches Bot information based on its bot token by calling the Bot API getMe
method. Open Program.cs
and use the following content:
⚠️ Replace
YOUR_BOT_TOKEN
with your bot token obtained from @BotFather.
using Telegram.Bot;
var bot = new TelegramBotClient("YOUR_BOT_TOKEN");
var me = await bot.GetMe();
Console.WriteLine($"Hello, World! I am user {me.Id} and my name is {me.FirstName}.");
Running the program gives you the following output:
dotnet run
Hello, World! I am user 1234567 and my name is Awesome Bot.
Great! This bot is self-aware. To make the bot react to user messages, head to the next page.
Your First Chat Bot
On the previous page we got a bot token and used the getMe
method to check our setup.
Now, it is time to make an interactive bot that gets users' messages and replies to them like in this screenshot:
Copy the following code to Program.cs
.
⚠️ Replace
YOUR_BOT_TOKEN
with the bot token obtained from @BotFather.
using Telegram.Bot;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using var cts = new CancellationTokenSource();
var bot = new TelegramBotClient("YOUR_BOT_TOKEN", cancellationToken: cts.Token);
var me = await bot.GetMe();
bot.OnMessage += OnMessage;
Console.WriteLine($"@{me.Username} is running... Press Enter to terminate");
Console.ReadLine();
cts.Cancel(); // stop the bot
// method that handle messages received by the bot:
async Task OnMessage(Message msg, UpdateType type)
{
if (msg.Text is null) return; // we only handle Text messages here
Console.WriteLine($"Received {type} '{msg.Text}' in {msg.Chat}");
// let's echo back received text in the chat
await bot.SendMessage(msg.Chat, $"{msg.From} said: {msg.Text}");
}
Run the program:
dotnet run
It runs waiting for text messages unless forcefully stopped by pressing Enter. Open a private chat with your bot in Telegram and send a text message to it. Bot should reply immediately.
By setting bot.OnMessage
, the bot client starts polling Telegram servers for messages received by the bot.
This is done automatically in the background, so your program continue to execute and we use Console.ReadLine()
to keep it running until you press Enter.
When user sends a message, the OnMessage(...)
method gets invoked with the Message
object passed as an argument (and the type of update).
We check Message.Type
and skip the rest if it is not a text message.
Finally, we send a text message back to the same chat we got the message from.
If you take a look at the console, the program outputs the chatId
numeric value.
In a private chat with you, it would be your userId
, so remember it as it's useful to send yourself messages.
Received Message 'test' in Private chat with @You (123456789).
Full Example
On the previous page we got a basic bot reacting to messages via bot.OnMessage
.
Now, we are going to set also bot.OnUpdate
and bot.OnError
to make a more complete bot
Modify your Program.cs
to the following:
using Telegram.Bot;
using Telegram.Bot.Polling;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using Telegram.Bot.Types.ReplyMarkups;
using var cts = new CancellationTokenSource();
var bot = new TelegramBotClient("YOUR_BOT_TOKEN", cancellationToken: cts.Token);
var me = await bot.GetMe();
bot.OnError += OnError;
bot.OnMessage += OnMessage;
bot.OnUpdate += OnUpdate;
Console.WriteLine($"@{me.Username} is running... Press Enter to terminate");
Console.ReadLine();
cts.Cancel(); // stop the bot
// method to handle errors in polling or in your OnMessage/OnUpdate code
async Task OnError(Exception exception, HandleErrorSource source)
{
Console.WriteLine(exception); // just dump the exception to the console
}
// method that handle messages received by the bot:
async Task OnMessage(Message msg, UpdateType type)
{
if (msg.Text == "/start")
{
await bot.SendMessage(msg.Chat, "Welcome! Pick one direction",
replyMarkup: new InlineKeyboardMarkup().AddButtons("Left", "Right"));
}
}
// method that handle other types of updates received by the bot:
async Task OnUpdate(Update update)
{
if (update is { CallbackQuery: { } query }) // non-null CallbackQuery
{
await bot.AnswerCallbackQuery(query.Id, $"You picked {query.Data}");
await bot.SendMessage(query.Message!.Chat, $"User {query.From} clicked on {query.Data}");
}
}
Run the program and send /start
to the bot.
note
/start
is the first message your bot receives automatically when a user interacts in private with the bot for the first time
The bot will reply with its welcome message and 2 inline buttons for you to choose.
When you click on a button, your bot receives an Update of type CallbackQuery that is not a simple message.
Therefore it will be handled by OnUpdate
instead.
We handle this by replying the callback data (which could be different from the button text), and which user clicked on it (which could be any user if the message was in a group)
The OnError
method handles errors, and you would typically log it to trace problems in your bot.
Look at the Console example in our Examples repository for an even more complete bot code.
Beginner
Sending Messages
There are many different types of message that a bot can send. Fortunately, methods for sending such messages are similar. Take a look at these examples:
Sending text message
await bot.SendMessage(chatId, "Hello, World!");
Sending sticker message
await bot.SendSticker(chatId, "https://telegrambots.github.io/book/docs/sticker-dali.webp");
Sending video message
await bot.SendVideo(chatId, "https://telegrambots.github.io/book/docs/video-hawk.mp4");
Text Messages and More
Text is a powerful interface for your bot and sendMessage
probably is the most used method of the Telegram Bot API.
Text messages are easy to send and fast to display on devices with slower networking.
Don't send boring plain text to users all the time. Telegram allows you to format the text using HTML or Markdown.
important
We highly recommend you use HTML instead of Markdown because Markdown has lots of annoying aspects
Send Text Message
The code snippet below sends a message with multiple parameters that looks like this:
You can use this code snippet in the event handler from Example Bot page and use
chatId
or put thechatId
value if you know it.
var message = await bot.SendMessage(chatId, "Trying <b>all the parameters</b> of <code>sendMessage</code> method",
ParseMode.Html,
protectContent: true,
replyParameters: update.Message.Id,
replyMarkup: new InlineKeyboardMarkup(
InlineKeyboardButton.WithUrl("Check sendMessage method", "https://core.telegram.org/bots/api#sendmessage")));
The method SendMessage
of .NET Bot Client maps to sendMessage
on Telegram's Bot API. This method sends a
text message and returns the message object sent.
text
is written in HTML format and parseMode
indicates that. You can also write in Markdown or plain text.
By passing protectContent
we prevent the message (and eventual media) to be copiable/forwardable elsewhere.
It's a good idea to make it clear to a user the reason why the bot is sending this message and that's why we pass the user's
message id for replyParameters
.
You have the option of specifying a replyMarkup
when sending messages.
Reply markups are explained in details later in this book.
Here we used an Inline Keyboard Markup with a button that attaches to the message itself. Clicking that opens
sendMessage
method documentation in the browser.
The Sent Message
Almost all of the methods for sending messages return you the message you just sent. Let's have a look at this object. Add this statement after the previous code.
Console.WriteLine(
$"{message.From.FirstName} sent message {message.Id} " +
$"to chat {message.Chat.Id} at {message.Date}. " +
$"It is a reply to message {message.ReplyToMessage.Id} " +
$"and has {message.Entities.Length} message entities.");
Output should look similar to this:
Awesome bot sent message 123 to chat 123456789 at 8/21/18 11:25:09 AM. It is a reply to message 122 and has 2 message entities.
There are a few things to note.
Date and time is in UTC format and not your local timezone.
Convert it to local time by calling message.Date.ToLocalTime()
method.
Message Entity refers to those formatted parts of the text: all the parameters in bold and
sendMessage in mono-width font.
Property message.Entities
holds the formatting information and message.EntityValues
gives you the actual value.
For example, in the message we just sent:
message.Entities.First().Type == MessageEntityType.Bold
message.EntityValues.First() == "all the parameters"
Try putting a breakpoint in the code to examine all the properties on a message objects you get.
Photo and Sticker Messages
You can provide the source file for almost all multimedia messages (e.g. photo, video) in 3 ways:
- Uploading a file with the HTTP request
- HTTP URL for Telegram to get a file from the internet
file_id
of an existing file on Telegram servers (recommended)
Examples in this section show all three. You will learn more about them later on when we discuss file upload and download.
Photo
Sending a photo is simple. Here is an example:
var message = await bot.SendPhoto(chatId, "https://telegrambots.github.io/book/docs/photo-ara.jpg",
"<b>Ara bird</b>. <i>Source</i>: <a href=\"https://pixabay.com\">Pixabay</a>", ParseMode.Html);
Caption
Multimedia messages can optionally have a caption attached to them. Here we sent a caption in HTML format. A user can click on Pixabay in the caption to open its URL in the browser.
Similar to message entities discussed before, caption entities on Message
object are the result of
parsing formatted(Markdown or HTML) caption text.
Try inspecting these properties in debug mode:
message.Caption
: caption in plain text without formattingmessage.CaptionEntities
: info about special entities in the captionmessage.CaptionEntityValues
: text values of mentioned entities
Photo Message
The message
returned from this method represents a photo message because message.Photo
has a value.
Its value is a PhotoSize
array with each element representing the same photo in different dimensions.
If your bot needs to send this photo again at some point, it is recommended to store this array
so you can reuse the file_id
value.
Here is how message.Photo
array looks like in JSON:
[
{
"file_id": "AgADBAADDqgxG-QDDVCm5JVvld7MN0z6kBkABCQawlb-dBXqBZUEAAEC",
"file_size": 1254,
"width": 90,
"height": 60
},
{
"file_id": "AgADBAADDqgxG-QDDVCm5JVvld7MN0z6kBkABAKByRnc22RmBpUEAAEC",
"file_size": 16419,
"width": 320,
"height": 213
},
{
"file_id": "AgADBAADDqgxG-QDDVCm5JVvld7MN0z6kBkABHezqGiNOz9yB5UEAAEC",
"file_size": 57865,
"width": 640,
"height": 426
}
]
Sticker
Telegram stickers are fun and our bot is about to send its very first sticker. Sticker files should be in WebP format.
This code sends the same sticker twice. First by passing HTTP URL to a WebP sticker file and
second by reusing FileId
of the same sticker on Telegram servers.
var message1 = await bot.SendSticker(chatId, "https://telegrambots.github.io/book/docs/sticker-fred.webp");
var message2 = await bot.SendSticker(chatId, message1.Sticker!.FileId);
Try inspecting the sticker1.Sticker
property. It is of type Sticker
and its schema looks similar to a photo.
There is more to stickers and we will talk about them in greater details later.
Audio and Voice Messages
These two types of messages are pretty similar. Audio is MP3-encoded file that can be played in music player. A voice file has OGG format and is not shown in music player.
Audio
This is the code to send an MP3 soundtrack. You might be wondering why some parameters are commented out? That's because this MP3 file has metadata on it and Telegram does a good job at reading it.
var message = await bot.SendAudio(chatId, "https://telegrambots.github.io/book/docs/audio-guitar.mp3"
// , performer: "Joel Thomas Hunger", title: "Fun Guitar and Ukulele", duration: 91 // optional
);
And a user can see the audio in Music Player:
Method returns an audio message. Let's take a look at the value of message.Audio
property in JSON format:
{
"duration": 91,
"mime_type": "audio/mpeg",
"title": "Fun Guitar and Ukulele",
"performer": "Joel Thomas Hunger",
"file_id": "CQADBAADKQADA3oUUKalqDOOcqesAg",
"file_size": 1102154
}
Voice
A voice message is an OGG audio file. Let's send it differently this time by uploading the file from disk alongside with an HTTP request.
To run this example, download the NFL Commentary voice file to your disk.
A value is passed for duration
because Telegram can't figure that out from a file's metadata.
⚠️ Replace
/path/to/voice-nfl_commentary.ogg
with an actual file path.
await using Stream stream = System.IO.File.OpenRead("/path/to/voice-nfl_commentary.ogg");
var message = await bot.SendVoice(chatId, stream, duration: 36);
A voice message is returned from the method. Inspect the message.Voice
property to learn more.
Video and Video Note Messages
You can send MP4 files as a regular video or a video note. Other video formats may be sent as documents.
Video
Videos, like other multimedia messages, can have caption, reply, reply markup, and etc. You can optionally specify the duration and resolution of the video.
In the example below, we send a video of a 10 minute countdown and expect the Telegram clients to stream that long video instead of downloading it completely. We also set a thumbnail image for our video.
await bot.SendVideo(chatId, "https://telegrambots.github.io/book/docs/video-countdown.mp4",
thumbnail: "https://telegrambots.github.io/book/2/docs/thumb-clock.jpg", supportsStreaming: true);
Check the Bot API docs for
sendVideo
method to learn more about video size limits and the thumbnail images.
User should be able to seek through the video without the video being downloaded completely.
Video Note
Video notes, shown in circles to the user, are usually short (1 minute or less) with the same width and height.
You can send a video note only by uploading the video file or reusing the file_id
of another video note.
Sending video note by its HTTP URL is not supported currently.
Download the Sea Waves video to your disk for this example.
await using Stream stream = System.IO.File.OpenRead("/path/to/video-waves.mp4");
await bot.SendVideoNote(chatId, stream,
duration: 47, length: 360); // value of width/height
Album Messages
Using sendMediaGroup
method you can send a group of photos, videos, documents or audios as an album. Documents and audio files can be only grouped in an album with messages of the same type.
var messages = await bot.SendMediaGroup(chatId, new IAlbumInputMedia[]
{
new InputMediaPhoto("https://cdn.pixabay.com/photo/2017/06/20/19/22/fuchs-2424369_640.jpg"),
new InputMediaPhoto("https://cdn.pixabay.com/photo/2017/04/11/21/34/giraffe-2222908_640.jpg"),
});
Document and Animation Messages
Send documents
Use sendDocument
method to send general files.
await bot.SendDocument(chatId, "https://telegrambots.github.io/book/docs/photo-ara.jpg",
"<b>Ara bird</b>. <i>Source</i>: <a href=\"https://pixabay.com\">Pixabay</a>", ParseMode.Html);
Send animations
Use sendAnimation
method to send animation files (GIF or H.264/MPEG-4 AVC video without sound).
await bot.SendAnimation(chatId, "https://telegrambots.github.io/book/docs/video-waves.mp4", "Waves");
Native Poll Messages
Native poll are a special kind of message with question & answers where users can vote. Options can be set to allow multiple answers, vote anonymously, or be a quizz with a correct choice and explanation.
Send a poll
This is the code to send a poll to a chat.
var pollMessage = await bot.SendPoll("@channel_name",
"Did you ever hear the tragedy of Darth Plagueis The Wise?",
new InputPollOption[]
{
"Yes for the hundredth time!",
"No, who`s that?"
});
You can optionally send a keyboard with a poll, both an inline or a regular one.
You'll get the message with Poll
object inside it.
Stop a poll
To close a poll you need to know original chat and message ids of the poll that you got from calling SendPoll
method.
Let's close the poll that we sent in the previous example:
Poll poll = await bot.StopPoll(pollMessage.Chat, pollMessage.Id);
You can add an inline keyboard when you close a poll.
As a result of the request you'll get the the final poll state with property Poll.IsClosed
set to true.
If you'll try to close a forwarded poll using message and chat ids from the received message even if your bot is the author of the poll you'll get an ApiRequestException
with message Bad Request: poll can't be stopped
. Polls originated from channels is an exception since forwarded messages originated from channels contain original chat and message ids inside properties Message.ForwardFromChat.Id
and Message.ForwardFromMessageId
.
Also if you'll try to close an already closed poll you'll get ApiRequestException
with message Bad Request: poll has already been closed
.
Other Messages
There are other kind of message types which are supported by the client. In the following paragraphs we will look how to send contacts, venues or locations.
Contact
This is the code to send a contact. Mandatory are the parameters chatId
, phoneNumber
and firstName
.
await bot.SendContact(chatId, phoneNumber: "+1234567890", firstName: "Han", lastName: "Solo");
If you want to send a contact as vCard you can achieve this by adding a valid vCard string
as value for the optional parameter vCard
as seen in the given example below.
await bot.SendContact(chatId, phoneNumber: "+1234567890", firstName: "Han",
vcard: "BEGIN:VCARD\n" +
"VERSION:3.0\n" +
"N:Solo;Han\n" +
"ORG:Scruffy-looking nerf herder\n" +
"TEL;TYPE=voice,work,pref:+1234567890\n" +
"EMAIL:hansolo@mfalcon.com\n" +
"END:VCARD");
Venue
The code snippet below sends a venue with a title and a address as given parameters:
await bot.SendVenue(chatId, latitude: 50.0840172f, longitude: 14.418288f,
title: "Man Hanging out", address: "Husova, 110 00 Staré Město, Czechia");
Location
The difference between sending a location and a venue is, that the venue requires a title and address. A location can be any given point as latitude and longitude.
The following snippet shows how to send a location with the mandatory parameters:
await bot.SendLocation(chatId, latitude: 33.747252f, longitude: -112.633853f);
Dealing with chats
All messages in Telegram are sent/received on a specific chat.
The chat.Type
can be one of 4 types:
ChatType.Private
:
A private discussion with a user. Thechat.Id
is the same as theuser.Id
(positive number)ChatType.Group
:
A private chat group with less than 200 usersChatType.Supergroup
:
An advanced chat group, capable of being public, supporting more than 200 users, with specific user/admin rightsChatType.Channel
:
A broadcast type of publishing feed (only admins can write to it)
Additional notes:
- For groups/channels, the
chat.Id
is a negative number, and thechat.Title
will be filled. - For public groups/channels, the
chat.Username
will be filled. - For private chat with a user, the
chat.FirstName
will be filled, and optionally, thechat.LastName
andchat.Username
if the user has one.
Calling chat methods
All methods for dealing with chats (like sending messages, etc..) take a ChatId
parameter.
For this parameter, you can pass directly a long
(the chat or user ID),
or when sending to a public group/channel, you can pass a "@chatname"
string
Getting full info about a chat (GetChat
)
Once a bot has joined a group/channel or has started receiving messages from a user, it can use method GetChat
to get detailed info about that chat/user.
There are lots of information returned depending on the type of chat, and most are optional and may be unavailable.
Here are a few interesting ones:
- For private chat with a User:
- Birthdate
- Personal channel
- Business information
- Bio
- For groups/channels:
- Description
- default Permissions (non-administrator access rights)
- Linked ChatId (the associated channel/discussion group for this chat)
- IsForum (This chat group has topics. There is no way to retrieve the list of topics)
- Common information for all chats:
- Photo (use
GetInfoAndDownloadFile
and thephoto.BigFileId
to download it) - Active Usernames (premium user & public chats can have multiple usernames)
- Available reactions in this chat
- Pinned Message (the most recent one)
- Photo (use
Receiving chat messages
See chapter Getting Updates for how to receive updates & messages.
For groups or private chats, you would receive an update of type UpdateType.Message
(which means only the field update.Message
will be set)
For channel messages, you would receive an update with field update.ChannelPost
.
For business messages, you would receive an update with field update.BusinessMessage
.
If someone modifies an existing message, you would receive an update with one of the fields update.Edited*
Note: if you use the bot.OnMessage
event, this is simplified and you can just check the UpdateType argument.
important
By default, for privacy reasons, bots in groups receive only messages that are targeted at them (reply to their messages, inline messages, or targeted /commands@botname
with the bot username suffix)
If you want your bot to receive ALL messages in the group, you can either make it admin, or disable the Bot Settings : Group Privacy mode in @BotFather
Migration to Supergroup
When you create a private chat group in Telegram, it is usually a ChatType.Group
.
If you change settings (like admin rights or making it public), or if members reach 200, the Group may be migrated into a Supergroup.
In such case, the Supergroup is like a separate chat with a different ID.
The old Group will have a service message MigrateToChatId
with the new supergroup ID.
The new Supergroup will have a service message MigrateFromChatId
with the old group ID.
Managing new members in a group
Bots can't directly add members into a group/channel.
To invite users to join a group/channel, you can send to the users the public link https://t.me/chatusername
(if chat has a username), or invite links:
Invite links
Invite links are typically of the form https://t.me/+1234567890aAbBcCdDeEfF
and allow users clicking on them to join the chat.
You can send those links as a text message or as an InlineKeyboardButton.WithUrl(...)
.
If your bot is administrator on a private (or public) group/channel, it can:
- read the (fixed) primary link of the chat:
var chatFullInfo = await bot.GetChat(chatId); // you should call this only once
Console.WriteLine(chatFullInfo.InviteLink);
- create new invite links on demand
var link = await bot.CreateChatInviteLink(chatId, "name/reason", ...);
Console.WriteLine(link.InviteLink);
See also some other methods for managing invite links.
Detecting new group members and changed member status
Note: Bots can't detect new channel members
The simpler approach to detecting new members joining a group is to handle service messages of type MessageType.NewChatMembers
: the field message.NewChatMembers
will contain an array of the new User details.
Same for a user leaving the chat, with the message.LeftChatMember
service message.
However, under various circumstances (bigger groups, hidden member lists, etc..), these service messages may not be sent out.
The more complex (and more reliable) approach is instead to handle updates of type UpdateType.ChatMember
:
- First you need to enable this specific update type among the
allowedUpdates
parameter when callingGetUpdates
,SetWebhook
orStartReceiving
+ReceiverOptions
. - Typically, you would pass
Update.AllTypes
as the allowedUpdates parameter. - After that, you will receive an
update.ChatMember
structure for each user changing status with their old & their new status - The
OldChatMember
/NewChatMember
status fields can be one of the derivedChatMember*
class:Owner
/Creator
,Administrator
,Member
,Restricted
,Left
,Banned
/Kicked
)
Reply Markup
Telegram provides two types of reply markup: Custom reply keyboards and Inline keyboards.
Custom reply keyboards
These are buttons visible below the textbox. Pressing such button will make the user send a message
Whenever your bot sends a message, it can pass along a special keyboard with predefined reply options. Regular keyboards are represented by ReplyKeyboardMarkup
object. You can request a contact or location information from the user with KeyboardButton
or send a poll. Regular button will send predefined text to the chat.
Keyboard is an array of button rows, each represented by an array of KeyboardButton
objects. KeyboardButton
supports text and emoji.
By default, reply keyboards are displayed until a new keyboard is sent by a bot.
Single-row keyboard markup
A ReplyKeyboardMarkup
with two buttons in a single row:
// using Telegram.Bot.Types.ReplyMarkups;
var replyMarkup = new ReplyKeyboardMarkup(true)
.AddButtons("Help me", "Call me ☎️");
var sent = await bot.SendMessage(chatId, "Choose a response", replyMarkup: replyMarkup);
We specify
true
on the constructor to resize the keyboard vertically for optimal fit (e.g., make the keyboard smaller if there are just two rows of buttons).
Multi-row keyboard markup
A ReplyKeyboardMarkup
with two rows of buttons:
// using Telegram.Bot.Types.ReplyMarkups;
var replyMarkup = new ReplyKeyboardMarkup(true)
.AddButton("Help me")
.AddNewRow("Call me ☎️", "Write me ✉️");
var sent = await bot.SendMessage(chatId, "Choose a response", replyMarkup: replyMarkup);
Requesting information to be sent to the bot
Some special keyboard button types can be used to request information from the user and send them to the bot.
Below are some simple examples of what you can do. More options are available in associated class properties.
KeyboardButton.WithRequestLocation("Share your location")
User's position will be transmitted in amessage.Location
KeyboardButton.WithRequestContact("Share your info")
User's phone number will be transmitted in amessage.Contact
KeyboardButton.WithRequestPoll("Create a poll", PollType.Regular)
User must create a poll which gets transmitted in amessage.Poll
KeyboardButton.WithRequestChat("Select a chat", 1234, false)
User must pick a group (false) or channel (true) which gets transmitted in amessage.ChatShared
KeyboardButton.WithRequestUsers("Select user(s)", 5678, 1)
User must pick 1-10 user(s) which get transmitted in amessage.UsersShared
KeyboardButton.WithWebApp("Launch WebApp", "https://www.example.com/game")
Launch a Mini-App
// using Telegram.Bot.Types.ReplyMarkups;
var replyMarkup = new ReplyKeyboardMarkup()
.AddButton(KeyboardButton.WithRequestLocation("Share Location"))
.AddButton(KeyboardButton.WithRequestContact("Share Contact"));
var sent = await bot.SendMessage(chatId, "Who or Where are you?", replyMarkup: replyMarkup);
Remove keyboard
To remove keyboard you have to send an instance of ReplyKeyboardRemove
object:
// using Telegram.Bot.Types.ReplyMarkups;
await bot.SendMessage(chatId, "Removing keyboard", replyMarkup: new ReplyKeyboardRemove());
Inline keyboards
These are buttons visible below a bot message. Pressing such button will NOT make the user send a message
There are times when you'd prefer to do things without sending any messages to the chat. For example, when your user is changing settings or flipping through search results. In such cases you can use Inline Keyboards that are integrated directly into the messages they belong to.
Unlike custom reply keyboards, pressing buttons on inline keyboards doesn't result in messages sent to the chat. Instead, inline keyboards support buttons that work behind the scenes: callback buttons, URL buttons and switch to inline buttons.
You can have several rows and columns of inline buttons of mixed types.
Callback buttons
When a user presses a callback button, no messages are sent to the chat, and your bot simply receives an update.CallbackQuery
instead.
Upon receiving this, your bot should answer to that query within 10 seconds, using AnswerCallbackQuery
(or else the button gets momentarily disabled)
In this example we use the AddButton(buttonText, callbackData)
helper, but you can also create such button with InlineKeyboardButton.WithCallbackData
:
// using Telegram.Bot.Types.ReplyMarkups;
var inlineMarkup = new InlineKeyboardMarkup()
.AddButton("1.1", "11") // first row, first button
.AddButton("1.2", "12") // first row, second button
.AddNewRow()
.AddButton("2.1", "21") // second row, first button
.AddButton("2.2", "22");// second row, second button
var sent = await bot.SendMessage(chatId, "A message with an inline keyboard markup",
replyMarkup: inlineMarkup);
URL buttons
Buttons of this type have a small arrow icon to help the user understand that tapping on a URL button will open an external link. In this example we use InlineKeyboardButton.WithUrl
helper method to create a button with a text and url.
// using Telegram.Bot.Types.ReplyMarkups;
var inlineMarkup = new InlineKeyboardMarkup()
.AddButton(InlineKeyboardButton.WithUrl("Repository Link", "https://github.com/TelegramBots/Telegram.Bot"));
var sent = await bot.SendMessage(chatId, "A message with an inline keyboard markup",
replyMarkup: inlineMarkup);
Switch to Inline buttons
Pressing a switch to inline button prompts the user to select a chat, opens it and inserts the bot's username into the input field. You can also pass a query that will be inserted along with the username – this way your users will immediately get some inline results they can share. In this example we use InlineKeyboardButton.WithSwitchInlineQuery
and InlineKeyboardButton.WithSwitchInlineQueryCurrentChat
helper methods to create buttons which will insert the bot's username in the chat's input field.
// using Telegram.Bot.Types.ReplyMarkups;
var inlineMarkup = new InlineKeyboardMarkup()
.AddButton(InlineKeyboardButton.WithSwitchInlineQuery("switch_inline_query"))
.AddButton(InlineKeyboardButton.WithSwitchInlineQueryCurrentChat("switch_inline_query_current_chat"));
var sent = await bot.SendMessage(chatId, "A message with an inline keyboard markup",
replyMarkup: inlineMarkup);
Other inline button types
Some more special inline button types can be used.
Below are some simple examples of what you can do. More options are available in associated class properties.
InlineKeyboardButton.WithCopyText("Copy info", "Text to copy"))
Store a text in the user clipboardInlineKeyboardButton.WithWebApp("Launch WebApp", "https://www.example.com/game"))
Launch a Mini-AppInlineKeyboardButton.WithLoginUrl("Login", new() { Url = "https://www.example.com/telegramAuth" }))
Authenticate the Telegram user via a website (Domain must be configured in @BotFather)InlineKeyboardButton.WithCallbackGame("Launch game"))
Launch an HTML game (Game must be configured in @BotFather)InlineKeyboardButton.WithPay("Pay 200 XTR"))
Customize the Pay button caption (only during a SendInvoice call)
Forward, Copy or Delete messages
You can forward, copy, or delete a single message, or even a bunch of messages in one go.
You will need to provide the source messageId(s), the source chatId and eventually the target chatId.
Note: When you use the plural form of the copy/forward methods, it will keep Media Groups (albums) as such.
Forward message(s)
You can forward message(s) from a source chat to a target chat (it can be the same chat). They will appear with a "Forwarded from" header.
// Forward a single message
await bot.ForwardMessage(targetChatId, sourceChatId, messageId);
// Forward an incoming message (from the update) onto a target ChatId
await bot.ForwardMessage(chatId, update.Message.Chat, update.Message.Id);
// Forward a bunch of messages from a source ChatId to a target ChatId, using a list of their message ids
await bot.ForwardMessages(targetChatId, sourceChatId, new int[] { 123, 124, 125 });
Copy message(s)
If you don't want the "Forwarded from" header, you can instead copy the message(s).
This will make them look like new messages.
// Copy a single message
await bot.CopyMessage(targetChatId, sourceChatId, messageId);
// Copy an incoming message (from the update) onto a target ChatId
await bot.CopyMessage(targetChatId, update.Message.Chat, update.Message.Id);
// Copy a media message and change its caption at the same time
await bot.CopyMessage(targetChatId, update.Message.Chat, update.Message.Id,
caption: "New <b>caption</b> for this media", parseMode: ParseMode.Html);
// Copy a bunch of messages from a source ChatId to a target ChatId, using a list of their message ids
await bot.CopyMessages(targetChatId, sourceChatId, new int[] { 123, 124, 125 });
Delete message(s)
Finally you can delete message(s).
This is particularly useful for cleaning unwanted messages in groups.
// Delete a single message
await bot.DeleteMessage(chatId, messageId);
// Delete an incoming message (from the update)
await bot.DeleteMessage(update.Message.Chat, update.Message.Id);
// Delete a bunch of messages, using a list of their message ids
await bot.DeleteMessages(chatId, new int[] { 123, 124, 125 });
Check if a message is a forward
When receiving an update about a message, you can check if that message is "Forwarded from" somewhere,
by checking if Message.ForwardOrigin
is set:
Console.WriteLine(update.Message.ForwardOrigin switch
{
MessageOriginChannel moc => $"Forwarded from channel {moc.Chat.Title}",
MessageOriginUser mou => $"Forwarded from user {mou.SenderUser}",
MessageOriginHiddenUser mohu => $"Forwarded from hidden user {mohu.SenderUserName}",
MessageOriginChat moch => $"Forwarded on behalf of {moch.SenderChat}",
_ => "Not forwarded"
});
Intermediate
Working with Updates & Messages
Getting Updates
There are two mutually exclusive ways of receiving updates for your bot — the long polling using getUpdates
method on one hand and Webhooks on the other. Telegram is queueing updates until the bot receives them either way, but they will not be kept longer than 24 hours.
- With long polling, the client is actively requesting updates from the server in a blocking way. The call returns if new updates become available or a timeout has expired.
- Setting a webhook means you supplying Telegram with a location in the form of an URL, on which your bot listens for updates. Telegram need to be able to connect and post updates to that URL.
Update types
Each user interaction with your bot results in an Update object.
It could be about a Message, some changed status, bot-specific queries, etc...
You can use update.Type
to check which kind of update you are dealing with.
However this property is slow and just indicates which field of update
is set, and the other fields are all null.
So it is recommended to instead directly test the fields of Update you want if they are non-null, like this:
switch (update)
{
case { Message: { } msg }: await HandleMessage(msg); break;
case { EditedMessage: { } editedMsg }: await HandleEditedMessage(editedMsg); break;
case { ChannelPost: { } channelMsg }: await HandleChannelMessage(channelMsg); break;
case { CallbackQuery: { } cbQuery }: await HandleCallbackQuery(cbQuery); break;
//...
}
Message types
If the Update is one of the 6 types of update containing a message (new or edited? channel? business?), the contained Message
object itself can be of various types.
Like above, you can use message.Type
to determine the type but it is recommended to directly test the non-null fields of Message
using if
or switch
.
There are a few dozens of message types, grouped in two main categories: Content and Service messages
Content messages
These messages represent some actual content that someone posted.
Depending on which field is set, it can be:
Text
: a basic text message (with itsEntities
for font effects, andLinkPreviewOptions
for preview info)Photo
,Video
,Animation
(GIF),Document
(file),Audio
,Voice
,PaidMedia
: those are media contents which can come with aCaption
subtext (and itsCaptionEntities
)VideoNote
,Sticker
,Dice
,Game
,Poll
,Venue
,Location
,Story
: other kind of messages without a caption
You can use methods message.ToHtml()
or message.ToMarkdown()
to convert the text/caption & entities into HTML (recommended) or Markdown.
Service messages
All other message types represent some action/status that happened in the chat instead of actual content.
We are not listing all types here, but it could be for example:
- members joined/left
- pinned message
- chat info/status/topic changed
- payment/passport/giveaway process update
- etc...
Common properties
There are additional properties that gives you information about the context of the message.
Here are a few important properties:
Id
: the ID that you will use if you need to reply or call a method acting on this messageChat
: in which chat the message arrivedFrom
: which user posted itDate
: timestamp of the message (in UTC)ReplyToMessage
: which message this is a reply toForwardOrigin
: if it is a Forwarded messageMediaGroupId
: albums (group of media) are separate consecutive messages having the same MediaGroupIdMessageThreadId
: the topic ID for Forum/Topic type chats
Example projects
Long polling
- Console application. Demonstrates a basic bot with some commands.
- Advanced console application. Demonstrates the use of many advanced programming features.
Webhook
- ASP.NET Core web application with Minimal APIs
- ASP.NET Core web application with Controllers
- Azure Functions
- AWS Lambda
Long Polling
Long Polling is done by calling getUpdates actively.
With our library, this can be done in one of three ways:
By setting bot.OnUpdate
(and/or bot.OnMessage
)
Setting those events will automatically start a background polling system which will call your events accordingly:
OnMessage
for updates about messages (new or edited Message, Channel Post or Business Messages)OnUpdate
for all other type of updates (callback, inline-mode, chat members, polls, etc..)
note
If you don't set OnMessage, the OnUpdate event will be triggered for all updates, including messages.
By using the StartReceiving
method (or ReceiveAsync
)
Those methods start a polling system which will call your method on incoming updates.
As arguments, you can pass either lambdas, methods or a class derived from IUpdateHandler
that implements the handling of Update and Error.
By calling GetUpdates
manually in a loop
You can specify a timeout so that the call blocks for up to X seconds, waiting for an incoming update
Here is an example implementation:
int? offset = null;
while (!cts.IsCancellationRequested)
{
var updates = await bot.GetUpdates(offset, timeout: 2);
foreach (var update in updates)
{
offset = update.Id + 1;
try
{
// put your code to handle one Update here.
}
catch (Exception ex)
{
// log exception and continue
}
if (cts.IsCancellationRequested) break;
}
}
Webhooks
With Webhook, your web application gets notified automatically by Telegram when new updates arrive for your bot.
Your application will receive HTTP POST requests with an Update structure in the body, using specific JSON serialization settings Telegram.Bot.JsonBotAPI.Options
.
Below, you will find how to configure an ASP.NET Core Web API project to make it work with Telegram.Bot, either with Controllers or Minimal APIs
⚠️ IMPORTANT: This guide describes configuration for versions 21.* and later of the library (based on System.Text.Json rather than NewtonsoftJson). If you're using older versions, you should upgrade first!
ASP.NET Core with Controllers (MVC)
First you need to configure your Web App startup code:
- Locate the line
services.AddControllers();
(in Program.cs or Startup.cs) - If you're using .NET 6.0 or more recent, add the line:
services.ConfigureTelegramBotMvc();
- For older .NET versions, add the line:
services.ConfigureTelegramBot<Microsoft.AspNetCore.Mvc.JsonOptions>(opt => opt.JsonSerializerOptions);
Next, in a controller class (like BotController.cs), you need to add an action for the updates. Typically:
[HttpPost]
public async Task HandleUpdate([FromBody] Update update)
{
// put your code to handle one Update here.
}
Good, now skip to SetWebHook below
ASP.NET Core with Minimal APIs
First you need to configure your Web App startup code:
- Locate the line
builder.Build();
(in Program.cs) - Above it, insert the line:
builder.Services.ConfigureTelegramBot<Microsoft.AspNetCore.Http.Json.JsonOptions>(opt => opt.SerializerOptions);
Next, you need to map an action for the updates. Typically:
app.MapPost("/bot", (Update update) => HandleUpdate(update));
...
async Task HandleUpdate(Update update)
{
// put your code to handle one Update here.
}
Good, now skip to SetWebHook below
Old ASP.NET 4.x support
For older .NET Framework usage, you may use the following code:
public async Task<IHttpActionResult> Post()
{
Update update;
using (var body = await Request.Content.ReadAsStreamAsync())
update = System.Text.Json.JsonSerializer.Deserialize<Update>(body, JsonBotAPI.Options);
await HandleUpdate(update);
return Ok();
}
SetWebHook
Your update handler code is ready, now you need to instruct Telegram to send updates to your URL, by running:
var bot = new TelegramBotClient("YOUR_BOT_TOKEN");
await bot.SetWebhook("https://your.public.host:port/bot", allowedUpdates: []);
You can now deploy your app to your webapp host machine.
Note: If you decide to switch back to Long Polling, remember to call bot.DeleteWebhook()
Common issues
- You need a supported certificate
If your host doesn't provide one, or you want to develop on your own machine, consider using ngrok:
See this useful step-by-step guide - You must use HTTPS (TLS 1.2+), IPv4, and ports 443, 80, 88, or 8443
- The Official webhook guide gives a lot of details
- If your update handler throws an exception or takes too much time to return,
Telegram will consider it a temporary failure and will RESEND the same update a bit later.
You may want to prevent handling the same update.Id twice:if (update.Id <= LastUpdateId) return; LastUpdateId = update.Id; // your code to handle the Update here.
- Most web hostings will recycle your app after some HTTP inactivity (= stop your app and restart it on the next HTTP request)
To prevent issues with this:- Search for an Always-On option with your host (usually not free)
- Make sure your web app can be safely stopped (saved state) and restarted later (reloading state)
- Make sure you don't have critical background code that needs to keep running at all time
- Have a service like cron-job.org ping your webapp every 5 minutes to keep it active. (host will likely still recycle your app after a few days)
- Host your app on a VPS machine rather than a webapp host.
Inline Mode
Telegram bots can be queried directly in the chat or via inline queries.
To use inline queries in your bot, you need to set up inline mode by command:
Import Telegram.Bot.Types.InlineQueryResults
namespace for inline query types.
There are two types that allow you to work with inline queries - InlineQuery
and ChosenInlineResult
:
switch (update.Type)
{
case UpdateType.InlineQuery:
await OnInlineQueryReceived(bot, update.InlineQuery!);
break;
case UpdateType.ChosenInlineResult:
await OnChosenInlineResultReceived(bot, update.ChosenInlineResult!);
break;
};
InlineQuery
Suppose we have two arrays:
private readonly string[] sites = { "Google", "Github", "Telegram", "Wikipedia" };
private readonly string[] siteDescriptions =
{
"Google is a search engine",
"Github is a git repository hosting",
"Telegram is a messenger",
"Wikipedia is an open wiki"
};
So we can handle inline queries this way:
async Task OnInlineQueryReceived(ITelegramBotClient bot, InlineQuery inlineQuery)
{
var results = new List<InlineQueryResult>();
var counter = 0;
foreach (var site in sites)
{
results.Add(new InlineQueryResultArticle(
$"{counter}", // we use the counter as an id for inline query results
site, // inline query result title
new InputTextMessageContent(siteDescriptions[counter])) // content that is submitted when the inline query result title is clicked
);
counter++;
}
await bot.AnswerInlineQuery(inlineQuery.Id, results); // answer by sending the inline query result list
}
InlineQueryResult
is an abstract type used to create a response for inline queries. You can use these result types for inline queries: InlineQueryResultArticle
for articles, InlineQueryResultPhoto
for photos, etc.
ChosenInlineResult
This type helps to handle chosen inline result. For example, you may want to know which result users chose:
Task OnChosenInlineResultReceived(ITelegramBotClient bot, ChosenInlineResult chosenInlineResult)
{
if (uint.TryParse(chosenInlineResult.ResultId, out var resultId) // check if a result id is parsable and introduce variable
&& resultId < sites.Length)
{
Console.WriteLine($"User {chosenInlineResult.From} has selected site: {sites[resultId]}");
}
return Task.CompletedTask;
}
To use the feature you need to enable "inline feedback" in BotFather by /setinlinefeedback
command:
Final result:
Working with Files
Downloading files
First, read the documentation on getFile
method.
To download file you have to know its file identifier - FileId
.
Finding the file identifier
Telegram Bot API has several object types, representing file:
PhotoSize
, Animation
, Audio
, Document
, Video
, VideoNote
, Voice
, Sticker
.
The file identifier for each file type can be found in their FileId
property (e.g. Message.Audio.FileId
).
The exception is photos, which represented as an array of PhotoSize[]
objects.
For each photo Telegram sends you a set of PhotoSize
objects - available resolutions, you can choose from.
Generally, you will want the highest quality - the last PhotoSize
object in the array.
With LINQ, this boils down to Message.Photo.Last().FileId
.
Downloading a file
Downloading a file from Telegram is done in two steps:
- Get file information with
getFile
method. ResultingFile
object containsFilePath
from which we can download the file. - Downloading the file.
var fileId = update.Message.Photo.Last().FileId;
var fileInfo = await bot.GetFile(fileId);
var filePath = fileInfo.FilePath;
The URL from which you can now download the file is https://api.telegram.org/file/bot<token>/<FilePath>
.
To download file you can use DownloadFile
function:
const string destinationFilePath = "../downloaded.file";
await using Stream fileStream = System.IO.File.Create(destinationFilePath);
await bot.DownloadFile(filePath, fileStream);
For your convenience the library provides you a helper function that does both - GetInfoAndDownloadFile
:
const string destinationFilePath = "../downloaded.file";
await using Stream fileStream = System.IO.File.Create(destinationFilePath);
var file = await bot.GetInfoAndDownloadFile(fileId, fileStream);
Uploading files
First, read the documentation on sending files.
Upload local file
To upload local file open stream and call one of the file-sending functions:
await using Stream stream = System.IO.File.OpenRead("../hamlet.pdf");
var message = await bot.SendDocument(chatId, document: InputFile.FromStream(stream, "hamlet.pdf"),
caption: "The Tragedy of Hamlet,\nPrince of Denmark");
Be aware of limitation for this method - 10 MB max size for photos, 50 MB for other files.
Upload file by file identifier
If the file is already stored somewhere on the Telegram servers, you don't need to reupload it: each file object has a FileId
property. Simply pass this FileId
as a parameter instead of uploading. There are no limits for files sent this way.
var fileId = update.Message.Photo.Last().FileId;
var message = await bot.SendPhoto(chatId, fileId);
Upload by URL
Provide Telegram with an HTTP URL for the file to be sent. Telegram will download and send the file. 5 MB max size for photos and 20 MB max for other types of content.
var message = await bot.SendPhoto(chatId, "https://cdn.pixabay.com/photo/2017/04/11/21/34/giraffe-2222908_640.jpg");
Stickers
Sticker
Telegram stickers are fun and our bot is about to send its very first sticker. Sticker files should be in WebP format.
This code sends the same sticker twice. First by passing HTTP URL to a WebP sticker file and
second by reusing FileId
of the same sticker on Telegram servers.
var message1 = await bot.SendSticker(chatId, "https://telegrambots.github.io/book/docs/sticker-fred.webp");
var message2 = await bot.SendSticker(chatId, message1.Sticker!.FileId);
Try inspecting the sticker1.Sticker
property. It is of type [Sticker
] and its schema looks similar to a photo.
Advanced topics
Telegram Login Widget
You can use InlineKeyboardButton.WithLoginUrl
to easily initiate a login connection to your website using the user's Telegram account credentials.
replyMarkup: new InlineKeyboardMarkup(InlineKeyboardButton.WithLoginUrl(
"login", new LoginUrl { Url = "https://yourdomain.com/url" }))
You'll need to associate your website domain with your bot by sending /setdomain
to @BotFather
.
See official documentation about Telegram Login Widget for more information.
Server-side, you can use our separate repository Telegram.Bot.Extensions.LoginWidget
to validate the user credentials, or to generate a Javascript to show the login widget directly on your website.
Working Behind a Proxy
TelegramBotClient
allows you to use a proxy for Bot API connections. This guide covers using three different proxy solutions.
If you are in a country, such as Iran, where HTTP and SOCKS proxy connections to Telegram servers are blocked, consider using a VPN, using Tor Network, or hosting your bot in other jurisdictions.
HTTP Proxy
You can configure HttpClient
with WebProxy
and pass it to the Bot client.
// using System.Net;
// using System.Net.Http;
WebProxy webProxy = new (Host: "https://example.org", Port: 8080)
{
// Credentials if needed:
Credentials = new NetworkCredential("USERNAME", "PASSWORD")
};
HttpClient httpClient = new (
new HttpClientHandler { Proxy = webProxy, UseProxy = true, }
);
var bot = new TelegramBotClient("YOUR_API_TOKEN", httpClient);
SOCKS5 Proxy
As of .NET 6, SocketsHttpHandler
is able to use Socks4, Socks4a and Socks5 proxies!
// using System.Net;
// using System.Net.Http;
WebProxy proxy = new ("socks5://127.0.0.1:9050")
{
Credentials = new NetworkCredential("USERNAME", "PASSWORD")
};
HttpClient httpClient = new (
new SocketsHttpHandler { Proxy = proxy, UseProxy = true, }
);
var bot = new TelegramBotClient("YOUR_API_TOKEN", httpClient);
SOCKS5 Proxy over Tor
Warning: Use for Testing only!
Do not use this method in a production environment as it has high network latency and poor bandwidth.
Using Tor, a developer can avoid network restrictions while debugging and testing the code before a production release.
-
Install Tor Browser
-
Open the
torcc
file with a text editor (Found inTor Browser\Browser\TorBrowser\Data\Tor
) -
Add the following lines: (configurations are described below)
EntryNodes {NL} ExitNodes {NL} StrictNodes 1 SocksPort 127.0.0.1:9050
-
Look at the Socks5 proxy example above.
-
Start the Tor Browser
Usage:
// using System.Net;
// using System.Net.Http;
WebProxy proxy = new ("socks5://127.0.0.1:9050");
HttpClient httpClient = new (
new SocketsHttpHandler { Proxy = proxy, UseProxy = true }
);
var bot = new TelegramBotClient("YOUR_API_TOKEN", httpClient);
Note that Tor has to be active at all times for the bot to work.
Configurations in torcc
EntryNodes {NL}
ExitNodes {NL}
StrictNodes 1
These three lines make sure you use nodes from the Netherlands as much as possible to reduce latency.
SocksPort 127.0.0.1:9050
This line tells tor to listen on port 9050 for any socks connections. You can change the port to anything you want (9050 is just the default), only make sure to use the same port in your code.
Business Bot Features
Several business features have been added for premium users to Telegram.
In particular, premium users can now select a bot to act as a chatbot on their behalf, in order to manage/reply to messages from other users (typically, their business customers).
BotFather configuration
First, the bot owner need to talk to @BotFather and go to the Bot Settings to enable Business Mode
In the following sections, we will refer to the premium user using your chatbot as "the business owner".
BusinessConnection update
Once your chatbot is configured, the business owner has to go to their account settings, under Telegram Business > Chatbots and type your bot username.
At this point, your bot will receive an update.BusinessConnection
which contains:
- a unique id (you may want to store this)
- details on the User (business owner)
- IsEnabled (false if the business connection got cancelled)
- CanReply (if the bot can act on behalf of that user in chats that were active in the last 24 hours)
You can retrieve these info again later using GetBusinessConnection(id)
BusinessMessage updates
From now on, your bot will receive updates regarding private messages between users (customers) and the business owner:
update.BusinessMessage
: a customer sent a new message to the business ownerupdate.EditedBusinessMessage
: a customer modified one of its message sent to the business ownerupdate.DeletedBusinessMessages
: a customer deleted some messages in private with the business owner
In these messages/updates, the field BusinessConnectionId
will tell you which BusinessConnection this applies to
(useful for context if your chatbot is used by several business owners)
Acting on behalf of the business owner
If the business owner enabled "Reply to message" during the initial business connection, your bot can reply or do some other actions on behalf of their user account.
To do so, you can call many Bot API methods with the optional businessConnectionId: parameter.
This way your bot can send/edit/pin messages, send chat actions (like "typing"), manage polls/live location, as if you were the business owner user.
Some notes about messages sent on behalf of the business owner:
- They will NOT be marked with your bot name from the customer point of view
- They will be marked with your bot name in the business owner private chat (a banner also appears on top of the chat)
- These features are limited to private chats initiated by customers talking to the business owner.
Bot Payments API & Telegram Stars
Telegram offers a safe, simple and unified payment system for goods and services.
Due to Google/Apple policies, there is a distinction between:
- Digital Goods & Services, which can be paid using Telegram Stars (XTR) only
- Physical Goods, which can be paid using real money, and can request more details like a shipping address.
Both process are similar, so we will demonstrate how to do a Telegram Stars payment (simpler) and give you some info about the difference for Physical Goods.
Important notes for physical goods
Before starting, you need to talk to @BotFather, select one of the supported Payment providers (you need to open an account on the provider website), and complete the connection procedure linking your bot with your provider account.
It is recommended to start with the Stripe TEST MODE provider so you can test your bot with fake card numbers before going live.
Price amounts are expressed as integers with some digits at the end for the "decimal" part.
For example, in USD currency there are 2 digits for cents, so 12345 means $123.45 ; With Telegram Stars (XTR), there are no extra digits.
See "exp" in this table, to determine the number of decimal digits for each currency.
Sending an invoice
When your bot is ready to issue a payment for the user to complete, it will send an invoice:
await bot.SendInvoice(
chatId: chatId, // same as userId for private chat
title: "Product Title",
description: "Product Detailed Description",
payload: "InternalProductID", // not sent nor shown to user
currency: "XTR", // 3-letters ISO 4217 currency
prices: [("Price", 500)], // only one price for XTR
photoUrl: "https://cdn.pixabay.com/photo/2012/10/26/03/16/painting-63186_1280.jpg",
);
Alternatively, you can instead generate an URL for that payment with CreateInvoiceLink
,
or if your bot supports Inline Mode, you can send invoices as inline results ("via YourBot").
With Physical Goods, you can specify more parameters like:
- the
providerToken
obtained by BotFather (something like "1234567:TEST:aBcDeFgHi") - several price lines detailing the total price
- some suggested tips
- the need for extra information about the user, including a shipping address
- if the price is flexible depending on the shipping address/method
Handling the ShippingQuery
Update
This update is received only for Physical Goods, if you specified a flexible price. Otherwise you can skip to the next section.
update.ShippingQuery
would contain information like the current shipping address for the user, and can be received again if the user changes the address.
You should check if the address is supported, and reply using bot.AnswerShippingQuery
with an error message or a list of shipping options with associated additional price lines:
var shippingOptions = new List<ShippingOption>();
shippingOptions.Add(new() { Title = "DHL Express", Id = "dhl-express",
Prices = [("Shipping", 1200)] });
shippingOptions.Add(new() { Title = "FedEx Fragile", Id = "fedex-fragile",
Prices = [("Packaging", 500), ("Shipping", 1800)] });
await bot.AnswerShippingQuery(shippingQuery.Id, shippingOptions);
Handling the PreCheckoutQuery
Update
This update is received when the user has entered their payment information and confirmed the final Pay button.
update.PreCheckoutQuery
contains all the requested information for the order, so you can validate that all is fine before actual payment
You must reply within 10 seconds with:
if (confirm)
await bot.AnswerPreCheckoutQuery(preCheckoutQuery.Id); // success
else
await bot.AnswerPreCheckoutQuery(preCheckoutQuery.Id, "Can't process your order: <REASON>");
Handling the SuccessfulPayment
Message
If you confirmed the order in the step above, Telegram requests the payment with the payment provider.
If the payment is successfully processed, you will receive a private Message of type SuccessfulPayment
from the user, and you must then proceed with delivering the goods or services to the user.
The message.SuccessfulPayment
structure contains all the same previous information, plus two payment identifiers from Telegram and from the Payment Provider.
You should store these ChargeId strings for traceability of the transaction in case of dispute, or refund (possible with RefundStarPayment).
Full example code for Telegram Stars transaction
using Telegram.Bot;
using Telegram.Bot.Types;
var bot = new TelegramBotClient("YOUR_BOT_TOKEN");
bot.OnUpdate += OnUpdate;
Console.ReadKey();
async Task OnUpdate(Update update)
{
switch (update)
{
case { Message.Text: "/start" }:
await bot.SendInvoice(update.Message.Chat,
"Unlock feature X", "Will give you access to feature X of this bot", "unlock_X",
"XTR", [("Price", 200)], photoUrl: "https://cdn-icons-png.flaticon.com/512/891/891386.png");
break;
case { PreCheckoutQuery: { } preCheckoutQuery }:
if (preCheckoutQuery is { InvoicePayload: "unlock_X", Currency: "XTR", TotalAmount: 200 })
await bot.AnswerPreCheckoutQuery(preCheckoutQuery.Id); // success
else
await bot.AnswerPreCheckoutQuery(preCheckoutQuery.Id, "Invalid order");
break;
case { Message.SuccessfulPayment: { } successfulPayment }:
System.IO.File.AppendAllText("payments.log", $"{DateTime.Now}: " +
$"User {update.Message.From} paid for {successfulPayment.InvoicePayload}: " +
$"{successfulPayment.TelegramPaymentChargeId} {successfulPayment.ProviderPaymentChargeId}\n");
if (successfulPayment.InvoicePayload is "unlock_X")
await bot.SendMessage(update.Message.Chat, "Thank you! Feature X is unlocked");
break;
};
}
Telegram Mini Apps
If standard Telegram Bot features aren't enough to fit your needs, you may want to consider building a Mini App instead.
This take the form of an integrated browser window showing directly web pages from your bot WebApp, so you have more control with HTML/JS to display the interface you like.
Check our full example project based on Razor pages, and including a clone of the above @DurgerKingBot and more demo to test features.
Starting Mini-Apps
Mini Apps can be launched from various ways:
- Keyboard Buttons:
KeyboardButton.WithWebApp
- Inline Buttons:
InlineKeyboardButton.WithWebApp
- Chat menu button (left of user textbox): via @BotFather or
SetChatMenuButton
- Inline-mode results with a "Switch to Mini App" button:
AnswerInlineQuery
with parameterInlineQueryResultsButton.WebApp
- Direct link like https://t.me/botusername/appname?startapp=command
Integration
Your web pages must include this script in the <head>
part:
<script src="https://telegram.org/js/telegram-web-app.js"></script>
Your Javascript can then access a Telegram.WebApp object supporting many properties and methods, as well as event handlers.
In particular, you may want to use your Telegram.Bot backend to validate the authenticity of Telegram.WebApp.initData
.
This can be done using our AuthHelpers.ParseValidateData
method and the bot token, to make sure the requests come from Telegram and obtain information about Telegram user and context.
For more details
To read more about Mini Apps, see https://core.telegram.org/bots/webapps
Visit our example project: https://github.com/TelegramBots/Telegram.Bot.Examples/tree/master/MiniApp
Telegram Passport
Telegram Passport is a unified authorization method for services that require personal identification. As a bot developer, you can use it to receive confidential user data in an end-to-end encrypted fashion. There are several Know Your Customer(KYC) solutions that have already added support for Telegram Passport.
This guide is targeted at bot developers and assumes the audience is already familiar with:
Telegram Passport - Quickstart
This guide teaches the basics of working with Telegram Passport. See the complete version of the code at Quickstart project. Code snippets on this page are in the context of that project.
Package
You need to add Telegram.Bot.Extensions.Passport
extension package to your project
in addition to the core package (Telegram.Bot
).
⭐️ Star the Telegram.Bot.Extensions.Passport project on GitHub 👍
dotnet add package Telegram.Bot.Extensions.Passport
Encryption Keys
You don't really need to generate any RSA key. Use our sample keys for this demo.
Send the public key to @BotFather using /setpublickey
command:
Copy this public key and send it to BotFather.
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0VElWoQA2SK1csG2/sY/
wlssO1bjXRx+t+JlIgS6jLPCefyCAcZBv7ElcSPJQIPEXNwN2XdnTc2wEIjZ8bTg
BlBqXppj471bJeX8Mi2uAxAqOUDuvGuqth+mq7DMqol3MNH5P9FO6li7nZxI1FX3
9u2r/4H4PXRiWx13gsVQRL6Clq2jcXFHc9CvNaCQEJX95jgQFAybal216EwlnnVV
giT/TNsfFjW41XJZsHUny9k+dAfyPzqAk54cgrvjgAHJayDWjapq90Fm/+e/DVQ6
BHGkV0POQMkkBrvvhAIQu222j+03frm9b2yZrhX/qS01lyjW4VaQytGV0wlewV6B
FwIDAQAB
-----END PUBLIC KEY-----
Now Telegram client app can encrypt the data for your bot using this key.
Request Information
Bot waits for a text message from user. Once it receives a text message, it generates an authorization request link and sends that to the user.
Authorization Request
A passport authorization request means that the bot should ask the user to open a tg://resolve
URI in the browser
with specific parameters in its query string.
You can alternatively have a button in an HTML page on your website for that.
Type AuthorizationRequestParameters helps you in creating such an URI.
AuthorizationRequestParameters authReq = new AuthorizationRequestParameters(
botId: 123456, // bot user ID
publicKey: "...", // public key in PEM format. same as the key above.
nonce: "unique nonce for this request",
scope: new PassportScope(new[] { // a PassportScope object
new PassportScopeElementOne("address"),
new PassportScopeElementOne("phone_number")
})
);
In SendAuthorizationRequestAsync method, we ask for address
and phone_number
scopes.
Then, we generate the query string and ask user to open the link.
You might be wondering what is the magic in here?
https://telegrambots.github.io/Telegram.Bot.Extensions.Passport/redirect.html
This web page redirects user to tg://resolve
URI, appending whatever query string was passed to it.
If a user is using an Android device, the URI will start with
tg:
instead of the defaulttg://
.
Passport Data
You, the user, should now be redirected to the Telegram Passport screen in your Telegram client app. Enter your password and log in.
Note that the app will ask you to register if this is the first time you are using Telegram Passport.
Fill in the address and phone number data. Click on the Authorize button at the end.
At this point, your Telegram client app encrypts the actual Telegram Passport data (e.g. address) using the AES algorithm, and then encrypts the info required for decryption using your bot's public RSA key. Finally, it sends the result of both encryptions to Telegram servers.
Data Decryption
Your bot now receives a new message update with the encrypted Passport data. The user is also notified in the chat:
Let's decrypt that gibberish to get the information. That's what DecryptPassportDataAsync method does.
Step 1: Credentials
You can't just access the encrypted data in the message.passport_data.data
array.
Required parameters for their decryption are in the message.passport_data.credentials
object.
But that credentials object is encrypted using bot's public key!
We first take the bot's private key this time and decrypt the credentials.
There are more details about importing a key in PEM format on the RSA Key page.
IDecrypter decrypter = new Decrypter();
Credentials credentials = decrypter.DecryptCredentials(
message.PassportData.Credentials, // EncryptedCredentials object
GetRsaPrivateKey() // private key as an RSA object
);
Step 2: Nonce
There is a nonce
property on the credentials (now decrypted) object.
In order to prevent certain attacks, ensure its value is exactly the same as the nonce you set in the authorization request.
Read more about nonce on Wikipedia.
Step 3: Residential Address
It's finally time to see the user's address.
We are looking for an encrypted element with type of address in message.passport_data.data
array.
Also, decryption parameters for that are in credentials.secure_data.address.data
.
Here is how the decryption magic happens:
EncryptedPassportElement addressElement = message.PassportData.Data.Single(
el => el.Type == PassportEnums.Scope.Address
);
ResidentialAddress address = decrypter.DecryptData<ResidentialAddress>(
encryptedData: addressElement.Data,
dataCredentials: credentials.SecureData.Address.Data
);
DecryptData method does 3 tasks here:
- Decrypts the data into a JSON-serialized string
- Verifies that the data hashes match
- Converts from JSON to a .NET object
Step 4: Phone Number
Values for phone number and email address are not end-to-end encrypted in Telegram Passport and Telegram stores these values after being verified.
There is no need for decryption at this point.
Just find the element with the type of phone_number in the message.passport_data.data
array.
Information Demo
At the end, bot sends some of the information received to the user for demo purposes.
Passport Files and Documents
We use the driver's license scope here to show decryption of ID document data and passport files for front side scan, reverse side scan, selfie photo, and translation scan. That should cover most of the field types in Telegram Passport.
Sections below are referring to the test methods in Driver's License Scope Tests collection. Here are the steps:
- Authorization Request
- Driver's License Info
- Passport Message
- Credentials
- ID Document Data
- Passport File
Authorization Request
We start by generating an authorization URI. Since a driver's license is considered as a proof of identity, we ask for optional data selfie with document and translation document scan as well.
Driver's License Info
As a user, provide information for the required fields: front side, reverse side, and document number. Also, test methods here expect a selfie photo and a file for translation scan.
Click the Authorize button at the end.
Passport Message
This test method checks for a Passport message with a driver's license element on it.
Credentials
We decrypt credentials using the RSA private key and verify that the same nonce is used.
RSA key = EncryptionKey.ReadAsRsa();
IDecrypter decrypter = new Decrypter();
Credentials credentials = decrypter.DecryptCredentials(
passportData.Credentials,
key
);
bool isSameNonce = credentials.Nonce == "Test nonce for driver's license";
ID Document Data
In our test case, there is only 1 item in the message.passport_data.data
array and that's the encrypted element for
the driver's license scope.
We can get information such as document number and expiry date for the license from that element:
IdDocumentData licenseDoc = decrypter.DecryptData<IdDocumentData>(
encryptedData: element.Data,
dataCredentials: credentials.SecureData.DriverLicense.Data
);
Passport File
Passport file is an encrypted JPEG file on Telegram servers. You need to download the passport file and decrypt it using its accompanying file credentials to see the actual JPEG file content. In this section we try to demonstrate different use cases that you might have for such files.
No matter the method used, the underlying decryption logic is the same. It really comes down to your decision on working with streams vs. byte arrays. IDecrypter gives you both options.
Front Side File
A pretty handy extension method is used here to stream writing the front side file to disk. Method DownloadAndDecryptPassportFileAsync does a few things:
- Makes an HTTP request to fetch the encrypted file's info using its passport file_id
- Makes an HTTP request to download the encrypted file using its file_path
- Decrypts the encrypted file
- Writes the actual content to the destination stream
File encryptedFileInfo;
using (System.IO.Stream stream = System.IO.File.OpenWrite("/path/to/front-side.jpg"))
{
encryptedFileInfo = await bot.DownloadAndDecryptPassportFileAsync(
element.FrontSide, // PassportFile object for front side
credentials.SecureData.DriverLicense.FrontSide, // front side FileCredentials
stream // destination stream for writing the JPEG content to
);
}
warning
This method is convenient to use but gives you the least amount of control over the operations.
Reverse Side File
Previous method call is divided into two operations here for reverse side of the license. Streams are used here as well.
File encryptedFileInfo;
using (System.IO.Stream
encryptedContent = new System.IO.MemoryStream(element.ReverseSide.FileSize),
decryptedFile = System.IO.File.OpenWrite("/path/to/reverse-side.jpg")
) {
// fetch the encrypted file info and download it to memory
encryptedFileInfo = await bot.GetInfoAndDownloadFile(
element.ReverseSide.FileId, // file_id of passport file for reverse side
encryptedContent // stream to copy the encrypted file into
);
// ensure memory stream is at the beginning before reading from it
encryptedContent.Position = 0;
// decrypt the file and write it to disk
await decrypter.DecryptFileAsync(
encryptedContent,
credentials.SecureData.DriverLicense.ReverseSide, // reverse side FileCredentials
decryptedFile // destination stream for writing the JPEG content to
);
}
Selfie File
We deal with selfie photo as a byte array. This is essentially the same operation as done above via streams. We also post the selfie photo to a chat.
// fetch the info of the passport file(selfie) residing on Telegram servers
File encryptedFileInfo = await bot.GetFile(element.Selfie.FileId);
// download the encrypted file and get its bytes
byte[] encryptedContent;
using (System.IO.MemoryStream
stream = new System.IO.MemoryStream(encryptedFileInfo.FileSize)
)
{
await bot.DownloadFile(encryptedFileInfo.FilePath, stream);
encryptedContent = stream.ToArray();
}
// decrypt the content and get bytes of the actual selfie photo
byte[] selfieContent = decrypter.DecryptFile(
encryptedContent,
credentials.SecureData.DriverLicense.Selfie
);
// send the photo to a chat
using (System.IO.Stream stream = new System.IO.MemoryStream(selfieContent)) {
await bot.SendPhoto(
123456,
stream,
"selfie with driver's license"
);
}
Translation File
A bot can request certified English translations of a document. Translations are also encrypted passport files so their decryption is no different from others passport files.
Assuming that the user sends one translation scan only for the license, we receive the translation passport file object in
message.passport_data.data[0].translation[0]
and its accompanying file credentials object in
credentials.secure_data.driver_license.translation[0]
.
File gets written to disk as a byte array.
PassportFile passportFile = element.Translation[0];
FileCredentials fileCreds = credentials.SecureData.DriverLicense.Translation[0];
// fetch passport file info
File encryptedFileInfo = await bot.GetFile(passportFile.FileId);
// download encrypted file and get its bytes
byte[] encryptedContent;
using (System.IO.MemoryStream
stream = new System.IO.MemoryStream(encryptedFileInfo.FileSize)
)
{
await bot.DownloadFile(encryptedFileInfo.FilePath, stream);
encryptedContent = stream.ToArray();
}
// decrypt the content and get bytes of the actual selfie photo
byte[] content = decrypter.DecryptFile(
encryptedContent,
fileCreds
);
// write the file to disk
await System.IO.File.WriteAllBytesAsync("/path/to/translation.jpg", content);
Passport Data Errors
If the passport data you received contains errors, the bot can use the SetPassportDataErrors method to inform the user and request information again. The user will not be able to resend the data, until all errors are fixed.
Here is an example call using decrypted credentials:
//using Telegram.Bot.Types.Passport;
PassportElementError[] errors =
{
new PassportElementErrorDataField
{
Type = EncryptedPassportElementType.Passport,
FieldName = "document_no",
DataHash = credentials.SecureData.Passport.Data.DataHash,
Message = "Invalid passport number"
},
new PassportElementErrorFrontSide
{
Type = EncryptedPassportElementType.Passport,
FileHash = credentials.SecureData.Passport.FrontSide.FileHash,
Message = "Document scan is redacted"
},
new PassportElementErrorSelfie
{
Type = EncryptedPassportElementType.Passport,
FileHash = credentials.SecureData.Passport.Selfie.FileHash,
Message = "Take a selfie without glasses"
},
new PassportElementErrorTranslationFile
{
Type = EncryptedPassportElementType.Passport,
FileHash = credentials.SecureData.Passport.Translation[0].FileHash,
Message = "Document photo is blury"
},
};
await bot.SetPassportDataErrors(passportMessage.From.Id, errors);
Import RSA Key
In order to decrypt the credentials you need to provide the private RSA key to DecryptCredentials method. If you have the RSA key in PEM format, you cannot simply instantiate an RSA .NET object from it. Here we discuss two ways of importing your PEM private key.
From PEM Format
This is the easier option and recommended for development time only. We can generate an RSA .NET object from an RSA Key in PEM format using the BouncyCastle package.
dotnet add package BouncyCastle
Code snippet here shows the conversion from a PEM file to the needed RSA object.
// using System.IO;
// using System.Security.Cryptography;
// using Org.BouncyCastle.Crypto;
// using Org.BouncyCastle.Crypto.Parameters;
// using Org.BouncyCastle.OpenSsl;
// using Org.BouncyCastle.Security;
static RSA GetPrivateKey() {
string privateKeyPem = File.ReadAllText("/path/to/private-key.pem");
PemReader pemReader = new PemReader(new StringReader(privateKeyPem));
AsymmetricCipherKeyPair keyPair = (AsymmetricCipherKeyPair) pemReader.ReadObject();
RSAParameters rsaParameters = DotNetUtilities
.ToRSAParameters(keyPair.Private as RsaPrivateCrtKeyParameters);
RSA rsa = RSA.Create(rsaParameters);
return rsa;
}
note
You don't necessarily need to have a dependency on the BouncyCastle package in your bot project. The section below offers a better alternative.
From RSA Parameters
We recommend to JSON-serialize RSAParameters of your key and create an RSA object using its values without any dependency on the BouncyCastle package in production deployment.
Copy EncryptionKeyUtility and EncryptionKeyParameters files from our Quickstart project. Those help with serialization.
You still need to use BouncyCastle only once to read the RSA key in PEM format and serialize its parameters:
// ONLY ONCE: read the RSA private key and serialize its parameters to JSON
static void WriteRsaParametersToJson() {
string privateKeyPem = System.IO.File.ReadAllText("/path/to/private-key.pem");
string json = EncryptionKeyUtility.SerializeRsaParameters(privateKeyPem);
System.IO.File.WriteAllText("/path/to/private-key-params.json", json);
}
// Now, read the JSON file and create an RSA instance
static RSA GetRsaKey() {
string json = System.IO.File.ReadAllText("/path/to/private-key-params.json");
return EncryptionKeyUtility.GetRsaKeyFromJson(json);
}
Content of private-key-params.json
will look similar to this:
{
"E": "AQAB",
"M": "0VElW...Fw==",
"P": "56Mdiw...i7FSwDaM=",
"Q": "51UN2sd...J44NTf0=",
"D": "nrXEeOl2Ky...JIQ==",
"DP": "KZYZWbsy.../lk60=",
"DQ": "Y25KgzPj...AdBd0=",
"IQ": "0153...N6Y="
}
It's worth mentioning that EncryptionKeyParameters is just a copy of RSAParameters struct. There are inconsistencies in serialization of RSAParameters type on different .NET platforms and that's why we use our own EncryptionKeyParameters type for serialization.
For instance, compare
RSAParameters
implementations on .NET Framework and .NET Core.
Telegram Passport Data Decryption - FAQ
What is PassportDataDecryptionException
Methods on IDecrypter
might throw PassportDataDecryptionException
exception
if an error happens during decryption.
The exception message tells you what went wrong but there is not much you can do to resolve it.
Maybe let your user know the issue and ask for Passport data again.
It is important to pass each piece of encrypted data, e.g. Id Document, Passport File, etc., with the right accompanying credentials to decryption methods.
Spot the problem in this code decrypting driver's license files:
byte[] selfieContent = decrypter.DecryptFile(
encSelfieContent, // byte array of encrypted selfie file
credentials.SecureData.DriverLicense.FrontSide // WRONG! use selfie file credentials
);
// throws PassportDataDecryptionException: "Data hash mismatch at position 123."
Frequently Asked Questions
I recommend you read all of these as you will learn many interesting things. Or you can use Ctrl-F to search for a specific topic.
- 1. Can you give me documentation/examples links?
- 2. My update handler fails or stops executing at some point
- 3. Apparently my update handler gets a
NullReferenceException
- 4. How to add buttons under a message?
- 5. How to handle a click on such inline buttons?
- 6. How to show a popup text to the user?
- 7. How to fill the input textbox of the user with some text?
- 8. How to fetch previous messages?
- 9. How to fetch a list of all users in chat?
- 10. How to send a private message to some random user?
- 11. How to detect if a user blocked my bot?
- 12. How to set a caption to a media group (album)?
- 13. How to write a bot that make questions/answers with users?
- 14. How to make font effects in message?
- 15. Where can I host my bot online for cheap/free?
- 16. Is there some limitation/maximum about feature X?
- 17. How to populate the bot Menu button / commands list?
- 18. How to receive
ChatMember
updates? - 19. How to get rid of past updates when I restart my bot?
- 20. Difficulties to upload & send a file/media?
- 21. How to fetch all medias from an album/media group ?
- 22. How to send a custom emoji❓
- 23. How to upgrade my existing code? You keep breaking compatibility!
- 24. Can I use several apps/instance to manage my bot?
- 25. How do I get the user id from a username?
- 26. How to receive messages from channels?
- 27. How to sent the same media multiple times
- This FAQ doesn't have my question on it
1. Can you give me documentation/examples links?
- Follow this installation guide to install the latest versions of the library.
- Here is the main documentation website.
- You can find more bot example projects here
- Search the official API documentation and official bots FAQ.
- check tooltips in your IDE, or navigate with F12 on API methods and read/expand comments.
If you're C# beginner, you should learn about async programming.
2. My update handler fails or stops executing at some point
You likely have an exception somewhere. You should place a try..catch
around your whole update handler.
Also, you should learn to use a debugger and go step-by-step through your code to understand where and why an exception is raised. See next question.
3. Apparently my update handler gets a NullReferenceException
Not all updates are about an incoming Message, so update.Message
could be null. (see also update.Type
)
Not all messages are text messages, message.Text
could be null (see also message.Type
). etc...
So please use a debugger to check the content of your variables or structure fields and make sure your code can handle all cases.
4. How to add buttons under a message?
Pass an InlineKeyboardMarkup into the replyMarkup
parameter when sending the message. You will likely need to create a List<List<InlineKeyboardButton>>
for rows&columns
See also next question.
5. How to handle a click on such inline buttons?
For buttons with callback data, your update handler should handle update.CallbackQuery
.
(Remember that not all updates are about update.Message
. See question #3)
Your code should answer to the query within 10 seconds, using AnswerCallbackQuery
(or else the button gets momentarily disabled)
6. How to show a popup text to the user?
It is only possible with inline callback button (see above questions).
Use AnswerCallbackQuery
with some text, and pass parameter showAlert: true
to display the text as an alert box instead of a short popup.
7. How to fill the input textbox of the user with some text?
There is not a simple direct method for this, but here is what you can try:
- With a Public username link:
t.me/username?text=Hello+World
(works only if target is a user/bot and not the current chat) - With a Share link:
tg://msg_url?url=https://example.com&text=Hello+World
(user must select a target first) - With a Bot deep link:
t.me/botusername?start=param
(param is limited to base64 characters, bot will receive/start param
) - With a ReplyKeyboardMarkup: buttons under the textbox to send pre-made texts
- With an Inline Mode bot and
SwitchInlineQuery
inline buttons, you can make the user pre-type the name of your bot followed by some query
8. How to fetch previous messages?
You can't with Bot API but it's possible with WTelegramBot.
Normally, bots only get messages at the moment they are posted. You could archive them all in a database for later retrieval.
9. How to fetch a list of all users in chat?
You can't with Bot API but it's possible with WTelegramBot.
Normally, bots can only get the list of admins (GetChatAdministrators
) or detail about one specific member (GetChatMember
)
Alternatively, you can keep track of users by observing new messages in a chat and saving user info into a database.
10. How to send a private message to some random user?
You can't. Bots can only send private messages to users that have already initiated a private chat with your bot.
11. How to detect if a user blocked my bot?
You would have received an update.MyChatMember
with NewChatMember.Status == ChatMemberStatus.Kicked
If you didn't record that info, you can try to SendChatAction
and see if it raises an exception.
12. How to set a caption to a media group (album)?
Set the media.Caption
(and media.ParseMode
) on the first media
13. How to write a bot that make questions/answers with users?
Either you can code a complex state machine workflow, saving where each user is currently in the discussion tree.
Or you can just use YourEasyBot which makes sequential bots very simple to write... (or one of the other frameworks available for Telegram.Bot)
14. How to make font effects in message?
Pass a ParseMode.Html
(or ParseMode.MarkDownV2
) to argument parseMode
. See formatting options.
⚠️ I highly recommend you choose HTML formatting because MarkDownV2 has A LOT of annoyingly reserved characters and you will regret it later.
15. Where can I host my bot online for cheap/free?
I would recommend you make an ASP.NET webhook bot and host it on some WebApp hosting service.
For example, Azure WebApp Service has a F1 Free plan including 1 GB disk, 1 GB ram, 60 minutes of daily cumulated active CPU usage (more than enough for most bots without heavy use). And publishing to Azure is very easy from VS.
A credit-card is necessary but you shouldn't get charged if you stay within quotas.
Other cloud providers might also offer similar services.
16. Is there some limitation/maximum about feature X?
See https://limits.tginfo.me for a list of limitations.
17. How to populate the bot Menu button / commands list?
You can either do this via @BotFather (static entries), or you can use SetMyCommands
for more advanced settings
⚠️ This menu can only be filled with bot commands, starting with a /
and containing only latin characters a-z_0-9
18. How to receive ChatMember
updates?
You should specify all update types including ChatMember in AllowedUpdates
array on StartReceiving
:ReceiverOptions
or SetWebhook
19. How to get rid of past updates when I restart my bot?
Pass true into StartReceiving
:ReceiverOptions
:DropPendingUpdates
or SetWebhook
:dropPendingUpdates
Alternatively, you can call await bot.DropPendingUpdates()
before polling or using bot.OnUpdate
.
20. Difficulties to upload & send a file/media?
- Make sure you
await
until the end of the send method before closing the file (a "using
" clause would close the file on leaving the current { scope } - If you just filled a
MemoryStream
, make sure to rewind it to the beginning withms.Position = 0;
before sending - If you send a media group, make sure you specify different filenames on
InputFile.FromStream
21. How to fetch all medias from an album/media group ?
Medias in a media group are received as separate consecutive messages having the same MediaGroupId
property. You should collect them progressively as you receive those messages.
There is no way to know how many medias are in the album, so:
- look for consecutive messages in that chat with same
MediaGroupId
and stop when it's not the same - stop after 10 medias in the group (maximum)
- use a timeout of a few seconds not receiving new messages in that chat to determine the end
22. How to send a custom emoji❓
⚠️ It costs about ~$5,000 !! 😱
- First you need to buy a reserved username on Fragment.
- Then you need to pay an additional upgrade fee of 1K TON to apply that username to your bot.
- Finally, your bot can now post custom emojis using specific HTML or Markdown syntax (or entity).
To post to a specific group, there is an alternative solution:
- Have premium members boost your group to Level 4.
- Then you can assign a custom emoji pack to your group that your members AND bots can use freely in group messages.
23. How to upgrade my existing code? You keep breaking compatibility!
A new lead developer (Wizou) is now in charge of the library and commits to reduce code-breaking changes in the future.
Version 21.x of the library and later have been much improved to facilitate migration from previous versions of the library, and include a lot of helpers/implicit operators to simplify your code.
24. Can I use several apps/instance to manage my bot?
You can call API methods (like sending messages) from several instances in parallel
BUT only one instance can call method GetUpdates (or else you will receive Telegram API Error 409: Conflict: terminated by other getUpdates request)
25. How do I get the user id from a username?
You can't with Bot API but it's possible with WTelegramBot.
Alternatively, you could store in database the mapping of UserId
<->Username
.
Remember that not every user has a username, and it can be changed.
26. How to receive messages from channels?
Your bot has to be added as administrator of the channel.
You will then receive the messages as update.ChannelPost
or update.EditedChannelPost
.
27. How to sent the same media multiple times
The first time, you will send the media with a stream (upload). Next times, you will use its FileId:
var sent = await bot.SendVideo(chatId, stream, ....);
var fileId = sent.Video.FileId
// next times:
await bot.SendVideo(chatId2, fileId, ...);
For photos, use sent.Photo[^1].FileId
This FAQ doesn't have my question on it
Feel free to join our Telegram group and ask your question there
Migration guides to newer versions of the library
- Migrate to v22.*
- Migrate to v21.*
- Migrate to v19.*
- Migrate to v18.*
- Migrate to v17.*
- Migrate to v14.*
Migration guide for version 22.x
If you're migrating from version 19.x, you might want to read our migration doc for v21 first. There were lots of interesting changes in versions v21.x.
⚠️ Breaking changes
We removed the Async
suffix from our API method names, and renamed SendTextMessageAsync
to SendMessage
.
This was done to match official API documentation and because all our TelegramBotClient methods are asynchronous (no real need to differenciate between async or non-async methods)
We also reordered a few optional parameters to move away the lesser used arguments later down the argument list (typically: message_thread_id
/entities
)
and move the more useful arguments up closer to the beginning (typically: replyParameters
/replyMarkup
)
This should make your life simpler as you won't need to resort to named arguments for the most commonly used method parameters.
Finally, we renamed property Message.MessageId
as just Message.Id
. (but MessageId will remain supported)
note
All the previous names are still supported at the moment, so your code should just run fine with version 22.0, except you will get warnings about "Obsolete" code while compiling.
📝 How to adapt your code for these changes
In order to port your code easily and get rid of compiler warnings, I suggest you use these 4 Find and Replace operations in your IDE
So start by opening the Edit > Find and Replace > Replace in Files panel (Ctrl+Shift+H)
Untick the checkboxes: Match case and Match whole word
Tick the checkbox: ✓ Use regular expressions
Select the scope to look in: Current project or Entire solution
(In the following, we suppose your TelegramBotClient variable is named bot
, please modify the expressions if necessary)
-
To remove explicit null arguments for messageThreadId/entities
Replace:(bot\.Send\w+Async\b.*,) null,
With:$1
🖱️Click on Replace All
🖱️Click on Replace All again -
To rename SendTextMessageAsync with SendMessage
Replace:\.SendTextMessageAsync\b
With:.SendMessage
🖱️Click on Replace All -
To remove the Async suffixes:
Replace:(bot\.\w+)Async\b
With:$1
🖱️Click on Replace All -
To rename the MessageId property:
Replace:(message)\.MessageId\b
With:$1.Id
🖱️Click on Replace All
(Depending on your variable naming convention, you might want to repeat that, replacing(message)
with(msg)
or something else)
The remaining effort to make your code compile should now be much reduced.
(maybe a few more parameter reordering if you didn't use named parameters)
Addendum: we also renamed method MakeRequest
Async to SendRequest
(if you use this non-recommended method)
What's new in version 22.0
- Support for Bot API 7.11
- Implicit conversions for single-field structures
For example classes containing just one string (like BotDescription, WebAppInfo or CopyTextButton) can be handled just as a string - Implicit conversion from FileBase classes to InputFile
This means you can pass an object like aVideo
orPhotoSize
directly as argument to a Send method and it will use its FileId - Helper method
GetFileIdType
It can tell you which type of object/media is referenced by a FileId string - Huge rewrite of our serialization code to make it more performant and straightforward.
- Updated System.Text.Json due to vulnerability CVE-2024-43485
Migration guide for version 21.x
Important notes:
- Don't bother about version 20, migrate directly to version 21.*
- You won't find this version on Nuget: See this guide to install it in your programs.
- Version 21.10 supports Bot API 7.9 (including Telegram Stars payments)
- Library is now based on System.Text.Json and doesn't depend on NewtonsoftJson anymore. (See below)
Renamed parameter replyToMessageId: → replyParameters:
That parameter was renamed and you can still pass a messageId for simple replies.
Or you can pass a ReplyParameters structure for more advanced reply configuration.
Renamed parameter disableWebPagePreview: → linkPreviewOptions:
That parameter was renamed and you can still pass true
to disable web preview.
Or you can pass a LinkPreviewOptions structure for more precise preview configuration.
Changed bool?
→ bool
Many boolean parameters or fields are now simply of type bool
.
In most cases, it shouldn't impact your existing code, or rather simplify it. Previously null
values are now just false
.
Changed ParseMode?
→ ParseMode
When you don't need to specify a ParseMode, just pass default
or ParseMode.None
.
Better backward-compatibility and simplification of code
We added/restored features & implicit conversions that make your code simpler:
InputFile
: just pass astring
/Stream
for file_id/url/stream content (as was possible in previous versions of Telegram.Bot)InputMedia*
: just pass anInputFile
when you don't need to associate caption or suchMessageId
: auto-converts to/fromint
(and also fromMessage
)ReactionType
: just pass astring
when you want to send an emojiReactionType
: just pass along
when you want to send a custom emoji (id)- Some other obvious implicit conversion operators for structures containing a single property
- No more enforcing
init;
properties, so you can adjust the content of fields as you wish or modify a structure returned by the API (before passing it back to the API if you want) - No more JSON "required properties" during deserialization, so your old saved JSON files won't break if a field is added/renamed.
- Restored some
MessageType
enum values that were removed (renamed) recently (easier compatibility)
MaybeInaccessibleMessage
This class hierarchy was introduced in Bot API 7.0 and broke existing code and added unnecessary complexity.
This was removed in our library v21 and you will just receive directly a Message (as before).
To identify an "inaccessible message", you can just check message.Type == MessageType.Unknown
or message.Date == default
.
Chat and ChatFullInfo
In previous versions, the big Chat
structure contained many fields that were filled only after a call to GetChatAsync.
This structure is now split into Chat
and ChatFullInfo
structures.
The new Chat
structure contains only common fields that are always filled.
The new ChatFullInfo
structure inherits from Chat
and is returned only by GetChatAsync method, with all the extra fields.
Request structures
Request structures (types ending with Request
) are NOT the recommended way to use the library in your projects.
They are to be considered as low-level raw access to Bot API structures for advanced programmers, and might change/break at any time in the future.
If you have existing code using them, you can use the MakeRequestAsync
method to send those requests.
(Other methods based on those requests will be removed soon)
Payments with Telegram Stars
To make a payment in Telegram Stars with SendInvoiceAsync, set the following parameters:
providerToken:
null
or""
currency:
"XTR"
prices:
with a single price- no tip amounts
Webhooks with System.Text.Json
The library now uses System.Text.Json
instead of NewtonsoftJson
.
To make it work in your ASP.NET projects, you should now:
- Remove package Microsoft.AspNetCore.Mvc.NewtonsoftJson from your project dependencies
- Follow our Webhook page to configure your web app correctly
Note: When deploying a .NET 6+ build via Docker, this introduced a dependency on ASP.NET Core. See this issue for methods to update your Dockerfile
InputPollOption in SendPollAsync
SendPollAsync now expect an array of InputPollOption instead of string.
But we added an implicit conversion from string to InputPollOption, so the change is minimal:
// before:
await bot.SendPollAsync(chatId, "question", new[] { "answer1", "answer2" });
// after:
await bot.SendPollAsync(chatId, "question", new InputPollOption[] { "answer1", "answer2" });
Global cancellation token (v21.2)
You can now specify a global CancellationToken
directly in TelegramBotClient constructor.
This way, you won't need to pass a cancellationToken to every method call after that (if you just need one single cancellation token for stopping your bot)
Polling system now catch exceptions in your HandleUpdate code (v21.3)
warning
That's a change of behaviour, but most of you will probably welcome this change
If you forgot to wrap your HandleUpdateAsync code in a big try..catch
, and your code happen to throw an exception,
this would previously stop the polling completely.
Now the Polling system will catch your exceptions, pass them to your HandleErrorAsync method and continue the polling.
In previous versions of the library:
- ReceiveAsync would throw out the exception (therefore stopping the polling)
- StartReceiving would pass the exception to HandlePollingErrorAsync and silently stop the polling
If you still want the previous behaviour, have your HandleErrorAsync start like this:
Task HandleErrorAsync(ITelegramBotClient bot, Exception ex, HandleErrorSource source, CancellationToken ct) { if (source is HandleErrorSource.HandleUpdateError) throw ex; ...
New helpers/extensions to simplify your code (v21.5)
- When replying to a message, you can now simply pass a
Message
for replyParameters: rather than aMessage.MessageId
Update.AllTypes
is a constant array containing allUpdateType
s. You can pass it for the allowedUpdates: parameter (GetUpdatesAsync
/SetWebhookAsync
)- Message has now 2 extensions methods:
.ToHtml()
and.ToMarkdown()
to convert the message text/caption and their entities into a simple Html or Markdown string. - You can also use methods
Markdown.Escape()
andHtmlText.Escape()
to sanitize reserved characters from strings - Reply/Inline Keyboard Markup now have construction methods to simplify building keyboards dynamically:
var replyMarkup = new InlineKeyboardMarkup()
.AddButton(InlineKeyboardButton.WithUrl("Link to Repository", "https://github.com/TelegramBots/Telegram.Bot"))
.AddNewRow().AddButton("callback").AddButton("caption", "data")
.AddNewRow("with", "three", "buttons")
.AddNewRow().AddButtons("A", "B", InlineKeyboardButton.WithSwitchInlineQueryCurrentChat("switch"));
- Same for ReplyKeyboardMarkup (and you can use
new ReplyKeyboardMarkup(true)
to resize keyboard)
As previously announced, the Request-typed methods are gone.
But you can still send Request structures via the MakeRequestAsync
method.
Simplified polling with events (v21.7)
Instead of StartReceiving
/ReceiveAsync
system, you can now simply set 2 or 3 events on the botClient:
bot.OnMessage += ...
to receive Message updatesbot.OnUpdate += ...
to receive other updates (or all updates if you don't setOnMessage
)bot.OnError += ...
to handle errors/exceptions during polling or your handlers
Note: Second argument to OnMessage
event specifies which kind of update it was (edited, channel or business message?)
When you assign those events, polling starts automatically, you don't have anything to do.
Polling will stop when you remove (-=
) your events, or when you cancel the global cancellation token
You can also use await bot.DropPendingUpdatesAsync()
before setting those events to ignore past updates.
The Console example project has been updated to demonstrate these events.
Automatic retrying API calls in case of "Too Many Requests" (v21.9)
If Telegram servers fail on your API call with this error and ask you to to retry in less than 60 seconds, TelegramBotClient will now automatically wait for the requested delay and retry sending the same request up to 3 times.
This is configurable using Options on the constructor:
using var cts = new CancellationTokenSource();
var options = new TelegramBotClientOptions(token) { RetryThreshold = 120, RetryCount = 2 };
var bot = new TelegramBotClient(options, cancellationToken: cts.Token);
*️⃣ This is a change of behavior compared to previous versions, but probably a welcome one.
To disable this system and keep the same behavior as previous versions, use RetryThreshold = 0
Notes for advanced users:
- If this happens while uploading files (InputFile streams), the streams will be reset to their start position in order to be sent again
- If your streams are non-seekable (no problem with MemoryStream/FileStream), the full HTTP request to Bot API will be buffered before the first sending (so it can lead to a temporary use of memory if you're sending big files)
Migration guide for version 19.0
Topics in Groups
New topics functionality allow bots interact with users in topic specified by messageThreadId
parameter.
We try to keep our Bot API implementation as close to Telegram Bot API as possible. This means, that the new messageThreadId
now the first optional parameter for a variety of methods.
Consider to use named parameters to avoid confusion with changed parameter order.
-Message message = await bot.SendTextMessageAsync(
- _fixture.SupergroupChat.Id,
- "Please click on *Notify* button.",
- cancellationToken);
+Message message = await bot.SendTextMessageAsync(
+ chatId: _fixture.SupergroupChat.Id,
+ text: "Please click on *Notify* button.",
+ messageThreadId: threadId,
+ cancellationToken: cancellationToken);
New InputFile Hierarchy
Old InputMedia*
class hierarchy poorly reflected actual file-related APIs.
We removed old hierarchy of InputFile
related classes such as InputOnlineFile
, InputTelegramFile
, InputFileStream
, etc., and also removed all implicit casts to them. From now on you should explicitly specify one of file types: InputFileStream
for Stream
content, InputFileUrl
for URL and InputFileId
if you want to use existing file_id
. For convenience the base InputType
class has factory methods to create the correct types:
InputFile.FromStream(Stream stream, string? fileName = default)
for streamsInputFile.FromString(string urlOrFileId)
for URLs or file idsInputFile.FromUri(Uri url)
- for URLs as stringsInputFile.FromUri(string url)
- for URLs asURI
sInputFile.FromFileId(string fileId)
- for file ids
The migration scheme looks like that:
Previous method | New method |
---|---|
new InputTelegramType(string) | InputFile.FromId(string) , InputFile.FromString(string) |
new InputTelegramType(Stream, string?) | InputFile.FromStream(Stream, string?) |
new InputFileStream(Stream) | InputFile.FromStream(Stream) |
new InputOnlineFile(string) | InputFile.FromId(string) , InputFile.FromString(string) , InputFile.FromString(string) , InputFile.FromUrl(string) , InputFile.FromUrl(Uri) |
new InputOnlineFile(Stream, string?) | InputFile.FromStream(Stream, string?) |
raw Stream | InputFile.FromStream(Stream) |
raw string | InputFile.FromString(string) |
raw URI | InputFile.FromUrl(URI) |
ChatId implicit conversion
Implicit conversion from ChatId
to string
was removed due to complaints and problems it caused. The migration path is to explicitly call ChatId.ToString()
method.
Stickers
- All methods and types with animated, static and video sticker distinction were removed and replaced with a single set of sticker related methods per new Bot API updates:
AddAnimatedStickerToSetAsync
,AddStaticStickerToSetAsync
,AddVideoStickerToSetAsync
, etc. Remove the wordsStatic
,Animated
andVideo
from sticker related methods in your code - Associated emojies and masks were moved to a separate type
InputSticker
, use them there instead, consult the official Bot API docs for a more detailed information
.NET Core 3.1 removed as a separate target framework
Since .NET Core 3.1 LTS status is not officialy supported anymore we changed the target to netstandard2.0
and net6.0
instead. If you're using .NET Core 3.1 or .NET 5 runtimes you need to use the build for netstandard2.0
instead. If you relied on IAsyncEnumerable
implementation of poller you need to move to .NET 6 instead.
Other changes
Message.Type
returnsMessageType.Animation
when the message contains anAnimation
, useMessageType.Animation
instead ofMessageType.Document
to check if the message contains an animation- Property
CanSendMediaMessages
was removed from the typesChatMemberRestricted
andChatPermissions
and replaced with more granular permissions, use them instead - Removed method
GetChatMembersCountAsync
, useGetChatMemberCountAsync
- Removed method
KickChatMemberAsync
, useBanChatMemberAsync
- Properties and types
VoiceChatEnded
,VoiceChatParticipantsInvited
,VoiceChatScheduled
,VoiceChatStarted
removed, use methods and types which start withVideo*
instead - All propties with the word
Thumb
in them were renamed to contain the wordThumbnail
per new Bot API updates - A new type
InlineQueryResultsButton
is used instead ofSwitchPmText
andSwitchPmParameter
properties, consult the official Bot API docs
Migration guide for version 18.0
Most breaking changes in v18 come from new Bot API changes, such as:
- In Bot API 6.0
voice_chat*
related message properties were deprecated in favour ofvideo_chat*
with the same semantics and shape. - With introduction of video stickers in Bot API 5.7 we needed a way
to separate methods for different sticker types. So static .WEBP
*StickerSet*
methods and requests were given aStatic
prefix. - Removed
untilDate
parameter fromTelegramBotClientExtensions.BanChatSenderChatAsync
method andUntilDate
property fromBanChatSenderChatRequest
class. - As of the next update some users will be able to upload up to 4GB files, so we changed
FileBase.FileSize
type tolong?
. - A new way of configuring the client.
ApiRequestEventArgs
contains full request data.
Complete list of changes is available in CHANGELOG
1. Removal of VoiceChat*
properties in Message
object
Telegram renamed voice_chat_*
properties in the Message
class and with video_chat_*
onces so we replaced
corresponding MessageType
enum members with the new ones.
Following properties in Message
class and corresponding enum members in MessageType
enum were changed:
-VoiceChatScheduled
-VoiceChatStarted
-VoiceChatEnded
-VoiceChatParticipantsInvited
+VideoChatScheduled
+VideoChatStarted
+VideoChatEnded
+VideoChatParticipantsInvited
Also property CanManageVoiceChats
in ChatMemberAdministrator
and PromoteChatMemberRequest
classes was renamed to
CanManageVideoChats
.
2. Renaming static sticker methods and classes
With introduction of video stickers in Bot API 5.7 we needed a way
to separate methods for different sticker types. We already used Animated
and Video
suffix for methods related to animated
and video stickers so we decided to do the same for the static stickers:
- Classes
CreateNewStickerSetRequest
andAddStickerToSetRequest
were renamed toCreateNewStaticStickerSetRequest
andAddStaticStickerToSetRequest
. - Methods
CreateNewStickerSetAsync
andAddStickerToSetAsync
where renamed toCreateStaticNewStickerSetAsync
andAddStaticStickerToSetAsync
.
3. Removal of untilDate
parameter from BanChatSenderChatAsync
method and UntilDate
property from BanChatSenderChatRequest
class
The untilDate
parameter from TelegramBotClientExtensions.BanChatSenderChatAsync
method and UntilDate
property from BanChatSenderChatRequest
class were removed from the Bot API.
4. Lifting of the FileSize limit
As of the next update some users will be able to upload up to 4GB files, so we changed FileBase.FileSize
type to long?
to accommodate this change.
5. A new way of client configuration
Starting with this release client configuration parameters should be passed through TelegramBotClientOptions
class.
You need to create an instance of TelegramBotClientOptions
and pass it to the client:
using Telegram.Bot;
var options = new TelegramBotClientOptions(
token: "<token>"
// pass an optional baseUrl if you want to use a custom bot server
baseUrl: "https://custombotserverdomain.com",
// pass an optional flag `true` if you want to use test environment
useTestEnvironment = true
);
var client = new TelegramBotClient(options);
If you don't know about test environment you can read more about it in the official documentation.
If you don't need extra configuration options you can still use the constructor that accepts a token and an instance of HttpClient
:
var client = new TelegramBotClient("<token>");
6. Polling functionality in the core library
The latest biggest change which is not a breaking one, but nevertheless worth a note: deprecation of
Telegram.Bot.Extensions.Polling
package.
All the functionality from the package was merged into the core library under namespace Telegram.Bot.Polling
.
Name of the method HandleErrorAsync
in IUpdateHandler
interface was quite confusing from the beginning since a lot
of people assumed they can handle all errors in it, but in reality it's used only for handling errors during polling.
We decided to give it a more appropriate name: HandlePollingErrorAsync
.
Migration guide for version 17.0
There are several breaking changes in v17:
- New exceptions handling logic
- Removal of update and message events
- Removal of API methods from
ITelegramBotClient
interface and moving them into extension methods in the same namespace (that shouldn't break anyone's sources as long as they don't employ reflection or make their own interface implementations) - Working with default enum values
These are the most user facing breaking changes you should be aware of during migration.
Let's dive deep on the migrations.
New exceptions handling logic
v17 brings a new base type for exceptions: RequestException
. ApiRequestException
inherits from RequestException
and is thrown only when an actual error response with the correct body is received from the Bot API. In other situations RequestException
will be thrown instead containing actual exception as InnerException
if there is one, e.g. serialization or connection-related exceptions.
If you used ApiRequestException
and HttpRequestException
to handle most exception now you have to replace HttpRequestException
with RequestException
and look for the inner exception. All valid errors with JSON body from Telegram are now thrown as ApiRequestException
including 429: Too Many Requests
.
Since 5XX
responses don't usually include correct JSON body they are thrown as RequestException
with HttpRequestException
inside.
Look at the following example on how to handle different kinds of exceptions. You might not need to implement everything as you see, it's there only for demonstration purposes.
try
{
await bot.SendTextMessageAsync(chatId, "Hello");
}
catch (ApiRequestException exception)
{
switch (exception.StatusCode)
{
case 400:
// Handle incorrect requests exceptions
break;
case 401:
// Handle incorrect bot token exception (revoked tokens)
break;
case 403:
// Handle authorization exceptions (blocked users, unaccessible chats, etc)
break;
case 429:
// Handle rate limiting exception
break;
default:
// Handle other errors with valid json body: it includes status code and description of the error
break;
}
}
catch (RequestException exception)
{
if (exception.InnerException is HttpRequestException httpRequestException)
{
// Handle connection exceptions or 5XX exceptions from the Bot API
}
else if (exception.InnerException is JsonSerializationException serializationException)
{
// Handle serialization exception when a request or a response can't be serialized for some reason
}
else
{
// Handle all other exceptions
}
}
catch (OperationCancelledException exception)
{
// Handle cancellation exception, e.g. when CancellationToken is cancelled
}
Removal of events
In v17 we removed events and introduced a new way of getting updates with Telegram.Bot.Extensions.Polling package. You can find an example in First Chat Bot article.
Removal of API methods from ITelegramBotClient
interface
This change shouldn't affect most users, the methods are still there, but instead of being implementations of the interface they are now extension methods. It makes the interface leaner and easier to implement for custom clients and for decorators (e.g. rate limiters implemented as decorators). There isn't really a migration path for those who used these for some reason.
Working with default enum values
We changed how we work with enums. The most notable change is the default value: there is none, all our enums are now start with 1 (exception UpdateType
and MessageType
since they are not a part of the Bot API and we fully control these). 0
value is left unassigned for a purpose: if we encounter an unknown value in the response from the Bot API we assign 0
as its value.
Let's imagine that Telegram adds new MessageEntity
value. From now on all unknown values can be handled in the default
case of switch
statement.
MessageEntity entity = message.Entities.First();
switch (entity.Type)
{
case MessageEntityType.Username:
// ...
break;
case MessageEntityType.Command:
// ...
break;
default:
// All unknown values will go there
break;
}
Also some default enums values were removed, e.g. ParseMode.Default
since we started using nullable types for every optional value and ParseMode.Default
lost it's use. If a message doesn't have any markup you'll receive null
in places where ParseMode
type was used or if you want to explicitly indicate an absence of markup pass null
instead.
Other breaking changes
Constructor accepting IWebProxy
We removed constructor accepting IWebProxy
. Now you have to configure HttpClient yourself to use proxy. You can find examples in Working Behind a Proxy article.
InputMediaType
Property Type
in IInputMedia
was changed to an enum InputMediaType
for easier discoverability. So if you relied string values like photo
, video
, animation
and so on now you need to switch to using enums. As a result you'll get autocomplete in IDEs and more predictability of what types of input media there are.
EncryptedPassportElementType
Property Type
of EncryptedPassportElement
was replace with an enum for the same reason with EncryptedPassportElementType
enum.
ChatMember
As part of Bot API 5.3 implementation ChatMember
type was split into a hierarchy of types with a discriminator field Status
. If you need to access some data from the derived class you should use pattern matching or type casting like this:
ChatMember member = ... //;
if (chatMember is ChatMemberKicked kickedMember)
{
// now you can access properties of a kicked chat member
if (kickedMember.Until is not null)
{
// do something with the value of Until
}
}
ChatId
Fields Identifier
and Username
are now get-only properties. It shouldn't break most people's code as it's not a source breaking change. If you used reflection to find these fields you should to look for properties now.
InlineQueryResultBase
Type InlineQueryResultBase
is renamed to InlineQueryResult
to match Bot API type hierarchy.
Nullability
From now on all properties that are optional will use nullable types, e.g. int?
, string?
, because default values of such properties might be an actual values and isn't distinguishable from a lack of value. From now if a property is null
you can be sure that it's value was not present in a response from the Bot API.
ReplyKeyboardMarkup
Since ResizeKeyboard
and OneTimeKeyboard
are optional, we removed them from ReplyKeyboardMarkup
constructor. You have to use object initialization syntax to configure these properties:
var replyKeyboardMarkup = new ReplyKeyboardMarkup(
new KeyboardButton[][]
{
new KeyboardButton[] { "1.1", "1.2" },
new KeyboardButton[] { "2.1", "2.2" },
})
{
ResizeKeyboard = true
};
Migration guide for version 14.x
Date and Time
All DateTime
values are now in UTC format. Here are some examples of usage:
// Use UTC time when making a request
await bot.KickChatMemberAsync(
chatId: -9876,
userId: 1234,
untilDate: DateTime.UtcNow.AddDays(2)
);
// Convert to local time (not recommended though)
DateTime localTime = update.Message.Date.ToLocalTime();
Keyboard Buttons
Many keyboard button types are removed from project. It is more convenient to use factory methods on KeyboardButton
and InlineKeyboardButton
classes.
Here are some examples:
// Message having an inline keyboard button with URL that redirects to a page
await bot.SendTextMessageAsync(
chatId: -9876,
text: "Check out the source code",
replyMarkup: new InlineKeyboardMarkup(
InlineKeyboardButton.WithUrl("Repository", "https://github.com/TelegramBots/Telegram.Bot")
)
);
// Message to a private chat having a 2-row reply keyboard
await bot.SendTextMessageAsync(
chatId: 1234,
text: "Share your contact & location",
replyMarkup: new ReplyKeyboardMarkup(
new [] { KeyboardButton.WithRequestContact("Share Contact") },
new [] { KeyboardButton.WithRequestLocation("Share Location") },
)
);
GetFileAsync()
Downloading a file from Telegram Bot API has 2 steps (see docs here):
- Get file info by calling
getFile
- Download file from
https://api.telegram.org/file/bot<token>/<file_path>
GetFileAsync()
is replaced by 3 methods. Method GetInfoAndDownloadFileAsync()
looks very similar to old GetFileAsync()
:
// Gets file info and saves it to "path/to/file.pdf"
using (var fileStream = System.IO.File.OpenWrite("path/to/file.pdf"))
{
File fileInfo = await bot.GetInfoAndDownloadFileAsync(
fileId: "BsdfgLg4Khdlsn-bldBD",
destination: fileStream
);
}
Note that calling the method
GetInfoAndDownloadFileAsync()
results in 2 HTTP requests (steps 1 and 2 above) being sent to the Bot API.
There are two more methods that assist you with downloading files:
// New version of GetFileAsync() only gets the file info (step 1)
File fileInfo = await bot.GetFileAsync("BsdfgLg4Khdlsn-bldBD");
// Download file from server (step 2)
using (var fileStream = System.IO.File.OpenWrite("path/to/file.pdf")) {
await bot.DownloadFileAsync(
filePath: fileInfo.FilePath,
destination: fileStream
);
}
GetUpdatesAsync()
, SetWebhookAsync()
Value All
is removed from enum Telegram.Bot.Types.Enums.UpdateType
. In order to get all kind of updates, pass an empty list such as Array.Empty<UpdateType>()
for allowedUpdates
argument.
SetWebhookAsync()
Parameter url
is required. If you intend to remove the webhook, it is recommended to use DeleteWebhookAsync()
instead. However, you could achieve the same result by passing string.Empty
value to url
argument.
AnswerInlineQueryAsync()
and InlineQueryResult
Classes InlineQueryResultNew
and InlineQueryResultCache
are removed. InlineQueryResult
has become the only shared base type for all inline query result classes.
Many shared and redundant properties are removed. This might require significant changes to your .cs
files if your bot is in inline mode. Fortunately, all input query results now have constructors with only the required properties as their parameters. This is the preferred way to instantiate input query result instances e.g.:
Instead of:
// bad way. easy to get exceptions
var documentResult = new InlineQueryResultDocument
{
Id = "some-id",
Url = "https://example.com/document.pdf",
Title = "Some title",
MimeType = "application/pdf"
};
You should use:
// good way
var documentResult = new InlineQueryResultDocument(
id: "some-id",
documentUrl: "https://example.com/document.pdf",
title: "Some title",
mimeType: "application/pdf"
);
SendMediaGroupAsync()
InputMediaType
is renamed to InputMedia
.
ToDo
Inline Message Overloads
Many inline message methods have been replaced with their overloads.
EditInlineMessageTextAsync
-->EditMessageTextAsync
ToDo
FileToSend
New classes have replaced FileToSend
struct.
InputFileStream
:InputTelegramFile
:InputOnlineFile
:
In many cases, you can use implicit casting to pass parameters.
Stream stream = System.IO.File.OpenRead("photo.png");
var message = await bot.SendPhotoAsync("chat id", stream);
string fileId = "file_id on Telegram servers";
var message = await bot.SendPhotoAsync("chat id", fileId);
ToDo. implicit casts
UpdateType
and MessageType
Values in these two enums are renamed e.g. UpdateType.MessageUpdate
is now UpdateType.Message
.
MessageType.Service
is removed. Now each type of message has its own MessageType
value e.g. when a chat member leaves a group, corresponding update contains a message type of MessageType.ChatMemberLeft
value.
VideoNote
Properties Width
and Height
are removed. Vide notes are squared and Length
property represents both width and height.
Constructor Parameters Instead of Public Setters
Many types now have the required parameters in their constructors. To avoid running into problems or getting exceptions, we recommend providing all required values in the constructor e.g.:
//bad way:
var markup = new InlineKeyboardMarkup
{
Keyboard = buttonsArray,
ResizeKeyboard = true
};
// better:
var markup = new InlineKeyboardMarkup(buttonsArray)
{
ResizeKeyboard = true
};