Telegram Bots Book

NuGet Repository

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

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 Father

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:

Example Image

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

text message screenshot

await bot.SendMessage(chatId, "Hello, World!");

Sending sticker message

sticker message screenshot

await bot.SendSticker(chatId, "https://telegrambots.github.io/book/docs/sticker-dali.webp");

Sending video message

video message screenshot

await bot.SendVideo(chatId, "https://telegrambots.github.io/book/docs/video-hawk.mp4");

Text Messages and More

send message method tests

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:

text message screenshot

You can use this code snippet in the event handler from Example Bot page and use chatId or put the chatId 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

send photo method photo tests

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);

photo message

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 formatting
  • message.CaptionEntities: info about special entities in the caption
  • message.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

send sticker method sticker tests

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);

sticker messages

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

audio tests

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

send audio method

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
    );

audio message

And a user can see the audio in Music Player:

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

send voice method

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);

voice message

A voice message is returned from the method. Inspect the message.Voice property to learn more.

Video and Video Note Messages

tests

You can send MP4 files as a regular video or a video note. Other video formats may be sent as documents.

Video

method sendVideo

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.

vide screenshot 1

User should be able to seek through the video without the video being downloaded completely.

vide screenshot 2

Video Note

method sendVideoNote

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

vide note screenshot

Album Messages

send media group method tests

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

sendDocument method tests

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

sendAnimation method tests

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 tests

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

sendPoll method

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?"
    });

native poll

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

stopPoll method

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);

closed native poll

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

send contact method send contacts tests

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");

send contact

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");

send vcard

Venue

send venue method send venue tests

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");

send contact

Location

send location method send location tests

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);

send contact

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. The chat.Id is the same as the user.Id (positive number)
  • ChatType.Group:
    A private chat group with less than 200 users
  • ChatType.Supergroup:
    An advanced chat group, capable of being public, supporting more than 200 users, with specific user/admin rights
  • ChatType.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 the chat.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, the chat.LastName and chat.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 the photo.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)

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 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 calling GetUpdates, SetWebhook or StartReceiving+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 derived ChatMember* class: Owner/Creator, Administrator, Member, Restricted, Left, Banned/Kicked)

Reply Markup

reply markup tests

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 a message.Location
  • KeyboardButton.WithRequestContact("Share your info")
    User's phone number will be transmitted in a message.Contact
  • KeyboardButton.WithRequestPoll("Create a poll", PollType.Regular)
    User must create a poll which gets transmitted in a message.Poll
  • KeyboardButton.WithRequestChat("Select a chat", 1234, false)
    User must pick a group (false) or channel (true) which gets transmitted in a message.ChatShared
  • KeyboardButton.WithRequestUsers("Select user(s)", 5678, 1)
    User must pick 1-10 user(s) which get transmitted in a message.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 clipboard
  • InlineKeyboardButton.WithWebApp("Launch WebApp", "https://www.example.com/game"))
    Launch a Mini-App
  • InlineKeyboardButton.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)

forward message method forward messages method

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)

copy message method copy messages method

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)

delete message method delete messages method

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

Update type

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

Message type

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 its Entities for font effects, and LinkPreviewOptions for preview info)
  • Photo, Video, Animation (GIF), Document (file), Audio, Voice, PaidMedia: those are media contents which can come with a Caption subtext (and its CaptionEntities)
  • 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 message
  • Chat: in which chat the message arrived
  • From: which user posted it
  • Date: timestamp of the message (in UTC)
  • ReplyToMessage: which message this is a reply to
  • ForwardOrigin: if it is a Forwarded message
  • MediaGroupId: albums (group of media) are separate consecutive messages having the same MediaGroupId
  • MessageThreadId: the topic ID for Forum/Topic type chats

Example projects

Long polling

Webhook

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)

Console application

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)

Advanced console application

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

Webhook guide

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)

ASP.NET example with Controllers

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

ASP.NET example 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

inline mode bot API inline queries example

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:

/setinline command in BotFather

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

inline query result bot API

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

chosen inline result bot API

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:

set inline feedback command

Final result:

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:

  1. Get file information with getFile method. Resulting File object contains FilePath from which we can download the file.
  2. 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

send sticker method sticker tests

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);

sticker messages

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.

Telegram Network

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.

  1. Install Tor Browser

  2. Open the torcc file with a text editor (Found in Tor Browser\Browser\TorBrowser\Data\Tor)

  3. Add the following lines: (configurations are described below)

    EntryNodes {NL}
    ExitNodes {NL}
    StrictNodes 1
    SocksPort 127.0.0.1:9050
    
  4. Look at the Socks5 proxy example above.

  5. 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

Bot Business Mode

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 owner
  • update.EditedBusinessMessage: a customer modified one of its message sent to the business owner
  • update.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

Bot Payments API Bot Payments 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

send invoice method

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

Shipping Query Update Answer Shipping Query method

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

Pre Checkout Query Update Answer Pre Checkout Query method

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

Successful Payment 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

Mini App bot API MiniApp example project

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 parameter InlineQueryResultsButton.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

nuget package nuget downloads repository

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

passport quickstart example

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

nuget 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:

Setting public key

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

Request Parameters type

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 default tg://.

Passport link 1 Passport link 2

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.

Passport login

Fill in the address and phone number data. Click on the Authorize button at the end.

Passport info 1 address info phone info Passport info 2

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

Passport Data type

Your bot now receives a new message update with the encrypted Passport data. The user is also notified in the chat:

Passport update

Let's decrypt that gibberish to get the information. That's what DecryptPassportDataAsync method does.

Step 1: Credentials

EncryptedCredentials type Credentials type

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

Credentials type Request Parameters type

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

EncryptedPassportElement type ResidentialAddress type

SecureData type SecureValue type DataCredentials type

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:

  1. Decrypts the data into a JSON-serialized string
  2. Verifies that the data hashes match
  3. Converts from JSON to a .NET object

Step 4: Phone Number

EncryptedPassportElement type

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.

info demo

Passport Files and Documents

driver's license scope tests

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:

  1. Authorization Request
  2. Driver's License Info
  3. Passport Message
  4. Credentials
  5. ID Document Data
  6. Passport File

Authorization Request

PassportScope type PassportScopeElementOne type

method Should_Generate_Auth_Link

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 passport link

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.

driver's license passport 1 driver's license passport 2 driver's license passport 3

Click the Authorize button at the end.

Passport Message

Passport Data type

method Should_validate_passport_update

This test method checks for a Passport message with a driver's license element on it.

Credentials

EncryptedCredentials type Credentials type

method Should_decrypt_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

IdDocumentData type

method Should_decrypt_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

PassportFile type FileCredentials type

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

method Should_decrypt_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:

  1. Makes an HTTP request to fetch the encrypted file's info using its passport file_id
  2. Makes an HTTP request to download the encrypted file using its file_path
  3. Decrypts the encrypted file
  4. 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

method Should_decrypt_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

method Should_decrypt_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

method Should_decrypt_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

setPassportDataErrors method Passport Element Errors tests

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

bouncy castle c# logo

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.

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 with ms.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

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)

  1. 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

  2. To rename SendTextMessageAsync with SendMessage
    Replace: \.SendTextMessageAsync\b
        With: .SendMessage
    🖱️Click on Replace All

  3. To remove the Async suffixes:
    Replace: (bot\.\w+)Async\b
        With: $1
    🖱️Click on Replace All

  4. 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 MakeRequestAsync 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 a Video or PhotoSize 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:

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 a string/Stream for file_id/url/stream content (as was possible in previous versions of Telegram.Bot)
  • InputMedia*: just pass an InputFile when you don't need to associate caption or such
  • MessageId: auto-converts to/from int (and also from Message)
  • ReactionType: just pass a string when you want to send an emoji
  • ReactionType: just pass a long 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 a Message.MessageId
  • Update.AllTypes is a constant array containing all UpdateTypes. 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() and HtmlText.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 updates
  • bot.OnUpdate += ... to receive other updates (or all updates if you don't set OnMessage)
  • 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 streams
  • InputFile.FromString(string urlOrFileId) for URLs or file ids
  • InputFile.FromUri(Uri url) - for URLs as strings
  • InputFile.FromUri(string url) - for URLs as URIs
  • InputFile.FromFileId(string fileId) - for file ids

The migration scheme looks like that:

Previous methodNew 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 StreamInputFile.FromStream(Stream)
raw stringInputFile.FromString(string)
raw URIInputFile.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 words Static, Animated and Video 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 returns MessageType.Animation when the message contains an Animation, use MessageType.Animation instead of MessageType.Document to check if the message contains an animation
  • Property CanSendMediaMessages was removed from the types ChatMemberRestricted and ChatPermissions and replaced with more granular permissions, use them instead
  • Removed method GetChatMembersCountAsync, use GetChatMemberCountAsync
  • Removed method KickChatMemberAsync, use BanChatMemberAsync
  • Properties and types VoiceChatEnded, VoiceChatParticipantsInvited, VoiceChatScheduled, VoiceChatStarted removed, use methods and types which start with Video* instead
  • All propties with the word Thumb in them were renamed to contain the word Thumbnail per new Bot API updates
  • A new type InlineQueryResultsButton is used instead of SwitchPmText and SwitchPmParameter 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:

  1. In Bot API 6.0 voice_chat* related message properties were deprecated in favour of video_chat* with the same semantics and shape.
  2. 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 a Static prefix.
  3. Removed untilDate parameter from TelegramBotClientExtensions.BanChatSenderChatAsync method and UntilDate property from BanChatSenderChatRequest class.
  4. As of the next update some users will be able to upload up to 4GB files, so we changed FileBase.FileSize type to long?.
  5. A new way of configuring the client.
  6. 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 and AddStickerToSetRequest were renamed to CreateNewStaticStickerSetRequest and AddStaticStickerToSetRequest.
  • Methods CreateNewStickerSetAsync and AddStickerToSetAsync where renamed to CreateStaticNewStickerSetAsync and AddStaticStickerToSetAsync.

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):

  1. Get file info by calling getFile
  2. 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
    };