Getting Started
This library is designed as a simple wrapper around the Telegram Bot API. It’s encouraged to read Telegram’s docs first to get an understanding of what Bots are capable of doing. They also provide some good approaches to solve common problems.
Installing
go get github.com/bssth/telegram-bot-api/v7
A Simple Bot
To walk through the basics, let’s create a simple echo bot that replies to your messages repeating what you said. Make sure you get an API token from @Botfather before continuing.
Let’s start by constructing a new BotAPI.
package main
import (
"os"
tgbotapi "github.com/bssth/telegram-bot-api/v7"
)
func main() {
bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN"))
if err != nil {
panic(err)
}
bot.Debug = true
}
Instead of typing the API token directly into the file, we’re using environment variables. This makes it easy to configure our Bot to use the right account and prevents us from leaking our real token into the world. Anyone with your token can send and receive messages from your Bot!
We’ve also set bot.Debug = true in order to get more information about the
requests being sent to Telegram. If you run the example above, you’ll see
information about a request to the getMe endpoint. The library
automatically calls this to ensure your token is working as expected. It also
fills in the Self field in your BotAPI struct with information about the
Bot.
Now that we’ve connected to Telegram, let’s start getting updates and doing things. We can add this code in right after the line enabling debug mode.
// Create a new UpdateConfig struct with an offset of 0. Offsets are used
// to make sure Telegram knows we've handled previous values and we don't
// need them repeated.
updateConfig := tgbotapi.NewUpdate(0)
// Tell Telegram we should wait up to 30 seconds on each request for an
// update. This way we can get information just as quickly as making many
// frequent requests without having to send nearly as many.
updateConfig.Timeout = 30
// Start polling Telegram for updates.
updates := bot.GetUpdatesChan(updateConfig)
// Let's go through each update that we're getting from Telegram.
for update := range updates {
// Telegram can send many types of updates depending on what your Bot
// is up to. We only want to look at messages for now, so we can
// discard any other updates.
if update.Message == nil {
continue
}
// Now that we know we've gotten a new message, we can construct a
// reply! We'll take the Chat ID and Text from the incoming message
// and use it to create a new message.
msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
// We'll also say that this message is a reply to the previous message.
// For any other specifications than Chat ID or Text, you'll need to
// set fields on the `MessageConfig`.
msg.ReplyParameters = &tgbotapi.ReplyParameters{
MessageID: update.Message.MessageID,
}
// Okay, we're sending our message off! We don't care about the message
// we just sent, so we'll discard it.
if _, err := bot.Send(msg); err != nil {
// Note that panics are a bad way to handle errors. Telegram can
// have service outages or network errors, you should retry sending
// messages or more gracefully handle failures.
panic(err)
}
}
Congratulations! You’ve made your very own bot!
Now that you’ve got some of the basics down, we can start talking about how the library is structured and more advanced features.
Library Structure
This library is generally broken into three components you need to understand.
Configs
Configs are collections of fields related to a single request. For example, if
one wanted to use the sendMessage endpoint, you could use the MessageConfig
struct to configure the request. There is a one-to-one relationship between
Telegram endpoints and configs. They generally have the naming pattern of
removing the send prefix and they all end with the Config suffix. They
generally implement the Chattable interface. If they can send files, they
implement the Fileable interface.
Helpers
Helpers are easier ways of constructing common Configs. Instead of having to
create a MessageConfig struct and remember to set the ChatID and Text,
you can use the NewMessage helper method. It takes the two required parameters
for the request to succeed. You can then set fields on the resulting
MessageConfig after it’s creation. They are generally named the same as
method names except with send replaced with New.
Methods
Methods are used to send Configs after they are constructed. Generally,
Request is the lowest level method you’ll have to call. It accepts a
Chattable parameter and knows how to upload files if needed. It returns an
APIResponse, the most general return type from the Bot API. This method is
called for any endpoint that doesn’t have a more specific return type. For
example, setWebhook only returns true or an error. Other methods may have
more specific return types. The getFile endpoint returns a File. Almost
every other method returns a Message, which you can use Send to obtain.
There’s lower level methods such as MakeRequest which require an endpoint and
parameters instead of accepting configs. These are primarily used internally.
If you find yourself having to use them, please open an issue.
Files
Telegram supports specifying files in many different formats. In order to accommodate them all, there are multiple structs and type aliases required.
All of these types implement the RequestFileData interface.
| Type | Description |
|---|---|
FilePath | A local path to a file |
FileID | Existing file ID on Telegram’s servers |
FileURL | URL to file, must be served with expected MIME type |
FileReader | Use an io.Reader to provide a file. Lazily read to save memory. |
FileBytes | []byte containing file data. Prefer to use FileReader to save memory. |
FilePath
A path to a local file.
file := tgbotapi.FilePath("tests/image.jpg")
FileID
An ID previously uploaded to Telegram. IDs may only be reused by the same bot that received them. Additionally, thumbnail IDs cannot be reused.
file := tgbotapi.FileID("AgACAgIAAxkDAALesF8dCjAAAa_…")
FileURL
A URL to an existing resource. It must be served with a correct MIME type to work as expected.
file := tgbotapi.FileURL("https://i.imgur.com/unQLJIb.jpg")
FileReader
Use an io.Reader to provide file contents as needed. Requires a filename for
the virtual file.
When BotAPI.MaxRetries is enabled, uploads are automatically retried on 429
Too Many Requests responses only if every uploaded file is a FileBytes or a
FilePath (FileID and FileURL are not uploaded and don’t prevent retries).
A FileReader stream is consumed during upload, so requests uploading a
FileReader, or any custom RequestFileData implementation, are never retried.
Use FileBytes or FilePath if you need automatic retries.
var reader io.Reader
file := tgbotapi.FileReader{
Name: "image.jpg",
Reader: reader,
}
FileBytes
Use a []byte to provide file contents. Generally try to avoid this as it
results in high memory usage. Also requires a filename for the virtual file.
var data []byte
file := tgbotapi.FileBytes{
Name: "image.jpg",
Bytes: data,
}
Important Notes
The Telegram Bot API has a few potentially unanticipated behaviors. Here are a few of them. If any behavior was surprising to you, please feel free to open a pull request!
Callback Queries
- Every callback query must be answered, even if there is nothing to display to the user. Failure to do so will show a loading icon on the keyboard until the operation times out.
ChatMemberUpdated
- In order to receive
ChatMemberupdates, you must explicitly addUpdateTypeChatMemberto yourAllowedUpdateswhen getting updates or setting your webhook.
Entities use UTF16
- When extracting text entities using offsets and lengths, characters can appear to be in incorrect positions. This is because Telegram uses UTF16 lengths while Golang uses UTF8. It’s possible to convert between the two, see issue #231 for more details.
GetUpdatesChan
- This method is very basic and likely unsuitable for production use. Consider creating your own implementation instead, as it’s very simple to replicate.
- This method only allows your bot to process one update at a time. You can spawn goroutines to handle updates concurrently or switch to webhooks instead. Webhooks are suggested for high traffic bots.
Nil Updates
- At most one of the fields in an
Updatewill be set to a non-nil value. When evaluating updates, you must make sure you check that the field is not nil before trying to access any of it’s fields.
Privacy Mode
- By default, bots only get updates directly addressed to them. If you need to get all messages, you must disable privacy mode with Botfather. Bots already added to groups will need to be removed and re-added for the changes to take effect. You can read more on the Telegram Bot API docs.
User and Chat ID size
- These types require up to 52 significant bits to store correctly, making a
64-bit integer type required in most languages. They are already
int64types in this library, but make sure you use correct types when saving them to a database or passing them to another language.
Examples
With a better understanding of how the library works, let’s look at examples showing off Telegram’s features:
- Command Handling — parse and respond to bot commands with replies and link preview controls.
- Keyboard — custom reply keyboards.
- Inline Keyboard — inline buttons and callback queries.
- Rich Messages — structured blocks and Markdown rich messages with embedded media.
- Ephemeral Messages — targeted messages visible only to a specific user.
- Poll Media — polls with photos, links, and media attached to options, descriptions, or quiz explanations.
- Business Connections — connecting to Telegram Business accounts and handling business messages.
Command Handling
This is a simple example of changing behavior based on a provided command.
package main
import (
"log"
"os"
tgbotapi "github.com/bssth/telegram-bot-api/v7"
)
func main() {
bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN"))
if err != nil {
log.Panic(err)
}
bot.Debug = true
log.Printf("Authorized on account %s", bot.Self.UserName)
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil { // ignore any non-Message updates
continue
}
if !update.Message.IsCommand() { // ignore any non-command Messages
continue
}
// Create a new MessageConfig. We don't have text yet,
// so we leave it empty.
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "")
msg.ReplyParameters = &tgbotapi.ReplyParameters{
MessageID: update.Message.MessageID,
}
// Extract the command from the Message.
switch update.Message.Command() {
case "help":
msg.Text = "I understand /sayhi, /status, and /docs."
case "docs":
msg.Text = "Documentation: https://bssth.github.io/telegram-bot-api/"
// Show a large preview of the documentation link above the text.
// PreferLargeMedia is ignored unless URL is set explicitly.
msg.LinkPreviewOptions = &tgbotapi.LinkPreviewOptions{
URL: "https://bssth.github.io/telegram-bot-api/",
PreferLargeMedia: true,
ShowAboveText: true,
}
case "sayhi":
msg.Text = "Hi :)"
case "status":
msg.Text = "I'm ok."
default:
msg.Text = "I don't know that command"
}
if _, err := bot.Send(msg); err != nil {
log.Panic(err)
}
}
}
Keyboard
This bot shows a numeric keyboard when you send a “open” message and hides it when you send “close” message.
package main
import (
"log"
"os"
tgbotapi "github.com/bssth/telegram-bot-api/v7"
)
var numericKeyboard = tgbotapi.NewReplyKeyboard(
tgbotapi.NewKeyboardButtonRow(
tgbotapi.NewKeyboardButton("1"),
tgbotapi.NewKeyboardButton("2"),
tgbotapi.NewKeyboardButton("3"),
),
tgbotapi.NewKeyboardButtonRow(
tgbotapi.NewKeyboardButton("4"),
tgbotapi.NewKeyboardButton("5"),
tgbotapi.NewKeyboardButton("6"),
),
)
func main() {
bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN"))
if err != nil {
log.Panic(err)
}
bot.Debug = true
log.Printf("Authorized on account %s", bot.Self.UserName)
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil { // ignore non-Message updates
continue
}
msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
msg.ReplyParameters = &tgbotapi.ReplyParameters{
MessageID: update.Message.MessageID,
}
switch update.Message.Text {
case "open":
msg.ReplyMarkup = numericKeyboard
case "close":
msg.ReplyMarkup = tgbotapi.NewRemoveKeyboard(true)
}
if _, err := bot.Send(msg); err != nil {
log.Panic(err)
}
}
}
Inline Keyboard
This bot waits for you to send it the message “open” before sending you an inline keyboard containing a URL and some numbers. When a number is clicked, it sends you a message with your selected number.
package main
import (
"log"
"os"
tgbotapi "github.com/bssth/telegram-bot-api/v7"
)
var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonURL("Documentation", "https://bssth.github.io/telegram-bot-api/"),
tgbotapi.NewInlineKeyboardButtonData("2", "2"),
tgbotapi.NewInlineKeyboardButtonData("3", "3"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("4", "4"),
tgbotapi.NewInlineKeyboardButtonData("5", "5"),
tgbotapi.NewInlineKeyboardButtonData("6", "6"),
),
)
func main() {
bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN"))
if err != nil {
log.Panic(err)
}
bot.Debug = true
log.Printf("Authorized on account %s", bot.Self.UserName)
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
// Loop through each update.
for update := range updates {
// Check if we've gotten a message update.
if update.Message != nil {
// Construct a new message from the given chat ID and containing
// the text that we received.
msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
msg.ReplyParameters = &tgbotapi.ReplyParameters{
MessageID: update.Message.MessageID,
}
// If the message was open, add a copy of our numeric keyboard.
switch update.Message.Text {
case "open":
msg.ReplyMarkup = numericKeyboard
}
// Send the message.
if _, err = bot.Send(msg); err != nil {
panic(err)
}
} else if update.CallbackQuery != nil {
// Respond to the callback query, telling Telegram to show the user
// a message with the data received.
callback := tgbotapi.NewCallback(update.CallbackQuery.ID, update.CallbackQuery.Data)
if _, err := bot.Request(callback); err != nil {
panic(err)
}
// And finally, send a message containing the data received.
if update.CallbackQuery.Message != nil {
msg := tgbotapi.NewMessage(update.CallbackQuery.Message.Chat.ID, update.CallbackQuery.Data)
if _, err := bot.Send(msg); err != nil {
panic(err)
}
}
}
}
}
Rich Messages
Rich messages (sendRichMessage) let bots send structured content formatted with Markdown, HTML, or layout blocks. Rich messages were introduced in Telegram Bot API 10.1; Bot API 10.2 added block-structured layouts (InputRichBlock) and explicit embedded media (InputRichMessageMedia), and Bot API 10.3 added interactive buttons (RichMessageButton).
Creating a Rich Message
Use the NewRichMessage helper, which accepts a chat ID and an InputRichMessage. Exactly one of Markdown, HTML, or Blocks must be provided.
Markdown with Embedded Media
You can write Markdown text and reference embedded media using tg:// URI schemes mapped to entries in the Media slice. Local files (FilePath), remote URLs (FileURL), or Telegram file IDs (FileID) are automatically handled:
package main
import (
"log"
"os"
tgbotapi "github.com/bssth/telegram-bot-api/v7"
)
func main() {
bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN"))
if err != nil {
log.Panic(err)
}
richMsg := tgbotapi.NewRichMessage(12345, tgbotapi.InputRichMessage{
Markdown: "# Product Announcement\n\nTake a look at our new feature:\n\n\nLearn more at https://example.com.",
Media: []tgbotapi.InputRichMessageMedia{
{
ID: "shot1",
Media: tgbotapi.NewInputMediaPhoto(tgbotapi.FilePath("tests/image.jpg")),
},
},
})
if _, err := bot.Send(richMsg); err != nil {
log.Panic(err)
}
}
Block-Structured Layout
When you need fine-grained control over layout and elements, build the message using InputRichBlock elements:
package main
import (
"log"
"os"
tgbotapi "github.com/bssth/telegram-bot-api/v7"
)
func main() {
bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN"))
if err != nil {
log.Panic(err)
}
richMsg := tgbotapi.NewRichMessage(12345, tgbotapi.InputRichMessage{
Blocks: []tgbotapi.InputRichBlock{
{
Type: tgbotapi.RichBlockTypeHeading,
Size: 1, // heading size, 1-6; 1 is the largest
Text: &tgbotapi.RichText{IsPlain: true, PlainText: "Release Notes"},
},
{
Type: tgbotapi.RichBlockTypeParagraph,
Text: &tgbotapi.RichText{IsPlain: true, PlainText: "Bot API 10.3 support is now available in telegram-bot-api."},
},
{
Type: tgbotapi.RichBlockTypeDivider,
},
{
Type: tgbotapi.RichBlockTypePre,
Text: &tgbotapi.RichText{IsPlain: true, PlainText: "go get github.com/bssth/telegram-bot-api/v7@latest"},
Language: "sh",
},
},
})
if _, err := bot.Send(richMsg); err != nil {
log.Panic(err)
}
}
The library automatically uploads files referenced in blocks or media lists via attach:// multipart form entries.
Ephemeral Messages
Telegram Bot API 10.2+ introduces ephemeral messages: messages that are visible only to a single recipient (ReceiverUserID) and the bot. Ephemeral messages can only be sent in group and supergroup chats, and their delivery is not guaranteed, especially if the recipient is offline.
Sending an Ephemeral Message
Send configs that support ephemeral messages embed EphemeralMessageParameters. Set ReceiverUserID to make the message visible exclusively to that user; leaving it zero sends an ordinary message.
When the ephemeral message answers a callback query, also set CallbackQueryID. Setting ReplaceCallbackQueryMessage shows the ephemeral message in place of the message with the button, but it must be false when the callback query comes from an ephemeral message (such messages must be edited with the EditEphemeralMessage* configs instead, see below).
package main
import (
"log"
"os"
tgbotapi "github.com/bssth/telegram-bot-api/v7"
)
func main() {
bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN"))
if err != nil {
log.Panic(err)
}
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
for update := range updates {
cq := update.CallbackQuery
if cq == nil {
continue
}
// Acknowledge the callback query.
callback := tgbotapi.NewCallback(cq.ID, "")
if _, err := bot.Request(callback); err != nil {
log.Println("callback error:", err)
}
// Message is nil for callback queries from inline messages, and
// ephemeral messages can only be sent in group and supergroup chats.
if cq.Message == nil || !(cq.Message.Chat.IsGroup() || cq.Message.Chat.IsSuperGroup()) {
continue
}
// Send an ephemeral message visible only to the user who clicked the button.
msg := tgbotapi.NewMessage(cq.Message.Chat.ID, "Only you can see this secret response!")
msg.ReceiverUserID = cq.From.ID
msg.CallbackQueryID = cq.ID
// Replace the original message only if it is a regular message:
// for callback queries from ephemeral messages this must be false.
msg.ReplaceCallbackQueryMessage = cq.Message.EphemeralMessageID == 0
sent, err := bot.Send(msg)
if err != nil {
log.Println("send error:", err)
continue
}
// The returned Message carries the ephemeral identifier needed to
// edit or delete the message later.
log.Printf("Sent ephemeral message %d to user %d", sent.EphemeralMessageID, cq.From.ID)
}
}
Editing and Deleting Ephemeral Messages
Ephemeral messages are addressed by the chat they were sent to, the user who received them, and their per-chat ephemeral identifier: the EphemeralMessageID field of the Message returned by bot.Send (not MessageID). The identifier may be reused for another ephemeral message after the message is deleted or expires.
Use bot.Request to edit or delete ephemeral messages:
msg := tgbotapi.NewMessage(chatID, "Only you can see this")
msg.ReceiverUserID = userID
sent, err := bot.Send(msg)
if err != nil {
log.Panic(err)
}
// Edit the ephemeral message's text.
edit := tgbotapi.NewEditEphemeralMessageText(chatID, userID, sent.EphemeralMessageID, "Updated text")
if _, err := bot.Request(edit); err != nil {
log.Panic(err)
}
// Delete the ephemeral message.
del := tgbotapi.NewDeleteEphemeralMessage(chatID, userID, sent.EphemeralMessageID)
if _, err := bot.Request(del); err != nil {
log.Panic(err)
}
The same applies to buttons on an ephemeral message: the callback query’s Message.EphemeralMessageID identifies the message to pass to the EditEphemeralMessage* configs.
Replying to Ephemeral Messages
ReplyParameters.EphemeralMessageID is the identifier of an incoming ephemeral message in the current chat. A reply to an ephemeral message must itself be ephemeral (set ReceiverUserID), and may only be sent within 15 seconds of the original message:
for update := range updates {
incoming := update.Message
if incoming == nil || incoming.EphemeralMessageID == 0 || incoming.From == nil {
continue
}
reply := tgbotapi.NewMessage(incoming.Chat.ID, "Replying to your ephemeral message")
// The reply must itself be ephemeral.
reply.ReceiverUserID = incoming.From.ID
reply.ReplyParameters = &tgbotapi.ReplyParameters{
EphemeralMessageID: incoming.EphemeralMessageID,
}
if _, err := bot.Send(reply); err != nil {
log.Println("reply error:", err)
}
}
Poll Media
Telegram Bot API 10.0+ allows attaching media to polls: to the poll description, to individual poll options, and to quiz explanations.
Attaching Media to Poll Options
Create a poll with NewPoll and assign media to individual options using the Media field on InputPollOption:
package main
import (
"log"
"os"
tgbotapi "github.com/bssth/telegram-bot-api/v7"
)
func main() {
bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN"))
if err != nil {
log.Panic(err)
}
poll := tgbotapi.NewPoll(12345, "Which logo do you like best?", "Option A", "Option B")
// Attach a local photo to the first option.
poll.Options[0].Media = tgbotapi.NewInputMediaPhoto(tgbotapi.FilePath("tests/image.jpg"))
// Attach a remote image URL to the second option.
poll.Options[1].Media = tgbotapi.NewInputMediaPhoto(tgbotapi.FileURL("https://example.com/logo-b.png"))
if _, err := bot.Send(poll); err != nil {
log.Panic(err)
}
}
Attaching Media to the Description and Explanation
You can also attach media to the poll description (Media) or to the quiz explanation (ExplanationMedia):
poll := tgbotapi.NewPoll(12345, "Identify this building:", "Eiffel Tower", "Empire State")
poll.Type = "quiz"
poll.CorrectOptionIDs = []int{0}
// Add a description to the poll and attach a photo to it.
poll.Description = "A famous landmark."
poll.Media = tgbotapi.NewInputMediaPhoto(tgbotapi.FilePath("tests/image.jpg"))
// Attach media to the explanation, which is shown when the user chooses an
// incorrect answer or taps the lamp icon.
poll.Explanation = "This is the Eiffel Tower in Paris."
poll.ExplanationMedia = tgbotapi.NewInputMediaPhoto(tgbotapi.FilePath("tests/image.jpg"))
if _, err := bot.Send(poll); err != nil {
log.Panic(err)
}
Supported Media Types
The following media types can be attached to poll options:
InputMediaAnimation(NewInputMediaAnimation)InputMediaLink(NewInputMediaLink)InputMediaLivePhoto(NewInputMediaLivePhoto)InputMediaLocation(NewInputMediaLocation)InputMediaPhoto(NewInputMediaPhoto)InputMediaSticker(NewInputMediaSticker)InputMediaVenue(NewInputMediaVenue)InputMediaVideo(NewInputMediaVideo)
Local file uploads (FilePath, FileBytes, FileReader) are automatically serialized via attach://poll-media-%d and uploaded as multipart form data.
Business Connections
Telegram Business accounts can connect bots to manage customer chats and respond on behalf of the business.
Receiving Connection Updates
When a user connects or disconnects a bot with their business account, Telegram delivers an update with update.BusinessConnection:
package main
import (
"log"
"os"
tgbotapi "github.com/bssth/telegram-bot-api/v7"
)
func main() {
bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN"))
if err != nil {
log.Panic(err)
}
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.BusinessConnection != nil {
bc := update.BusinessConnection
log.Printf("Business connection %s from user %d (enabled: %t)", bc.ID, bc.User.ID, bc.IsEnabled)
if bc.Rights != nil {
log.Printf("Can reply: %t", bc.Rights.CanReply)
}
}
}
}
Handling Business Messages
Messages in the chats of a connected business account arrive in update.BusinessMessage. This includes the account owner’s own outgoing messages and messages sent on the owner’s behalf by a business bot (SenderBusinessBot), so an auto-reply should skip those and only answer messages from other users. The owner is the User of the BusinessConnection.
To send a message or reply on behalf of the business, assign the BusinessConnectionID from the incoming message:
// owners maps each business connection ID to the ID of the business
// account owner.
owners := make(map[string]int64)
for update := range updates {
if bc := update.BusinessConnection; bc != nil {
owners[bc.ID] = bc.User.ID
}
bm := update.BusinessMessage
if bm == nil || bm.From == nil {
continue
}
// Skip messages sent on behalf of the business account by a bot.
if bm.SenderBusinessBot != nil {
continue
}
ownerID, ok := owners[bm.BusinessConnectionID]
if !ok {
bc, err := bot.GetBusinessConnection(tgbotapi.GetBusinessConnectionConfig{
BusinessConnectionID: bm.BusinessConnectionID,
})
if err != nil {
log.Println("failed to get business connection:", err)
continue
}
ownerID = bc.User.ID
owners[bm.BusinessConnectionID] = ownerID
}
// Skip the account owner's own outgoing messages.
if bm.From.ID == ownerID {
continue
}
log.Printf("Received business message from [%s]: %s", bm.From.UserName, bm.Text)
// Reply on behalf of the business account.
reply := tgbotapi.NewMessage(bm.Chat.ID, "Thanks for contacting our business! We will get back to you shortly.")
reply.BusinessConnectionID = bm.BusinessConnectionID
reply.ReplyParameters = &tgbotapi.ReplyParameters{
MessageID: bm.MessageID,
}
if _, err := bot.Send(reply); err != nil {
log.Println("failed to reply:", err)
}
}
Inspecting Business Connections
You can also retrieve connection details at any time using GetBusinessConnection:
bc, err := bot.GetBusinessConnection(tgbotapi.GetBusinessConnectionConfig{
BusinessConnectionID: connectionID,
})
if err != nil {
log.Panic(err)
}
log.Printf("Connection %s is active: %t", bc.ID, bc.IsEnabled)
Change Log
v7.0.0
- Module path is now
github.com/bssth/telegram-bot-api/v7. See the v6 → v7 section of BREAKING.md for the full migration guide. - Add
context.Contextsupport:MakeRequestWithContext,UploadFilesWithContext,RequestWithContextandSendWithContext. The existing methods call them withcontext.Background().GetUpdatesWithContext,GetMeWithContext,GetFileWithContextandSendMediaGroupWithContext.- Generic
RequestTyped[T]andRequestTypedWithContext[T], which decode a request’s result intoT. GetUpdatesChanWithContext: cancelling the context aborts the running long poll (and the wait before a retry) and closes the channel.- Cancelling an upload returns the context’s error, and the uploaded file is closed on every path.
StopReceivingUpdatesis idempotent and polling can be restarted on the sameBotAPI. It now stops only pollers that are already running: calling it beforeGetUpdatesChanno longer makes that call return a closed channel. UseGetUpdatesChanWithContextfor race-free shutdown, and setUpdateConfig.Offsetto the last update ID plus one when restarting.- Add optional automatic retry on 429 Too Many Requests:
BotAPI.MaxRetries(default 0, disabled) retries after theretry_afterTelegram asks for. The wait honours the request context.BotAPI.MaxRetryAfterlimits how long the bot is willing to wait; a longerretry_afterreturns the 429 error immediately.- Uploads are retried only when every uploaded file is a
FileBytesor aFilePath. IsTooManyRequests(err)reports whether an error is a 429 and returns itsretry_after.
- Verify the
X-Telegram-Bot-Api-Secret-Tokenheader of webhook requests:- Set
BotAPI.WebhookSecretTokento theSecretTokenpassed tosetWebhook.HandleUpdatethen returnsErrBadSecretToken, andListenForWebhook/ListenForWebhookRespReqFormatanswer 401 Unauthorized, when the header is missing or wrong. Nothing changes while the field is empty. - Webhook error responses now carry their
Content-Type: application/jsonheader.
- Set
- Remove deprecated methods and fields:
BotAPI.GetChatMembersCount: useGetChatMemberCount.BanChatSenderChatConfig.UntilDate: no longer part ofbanChatSenderChat.InlineQueryResultPhoto.MimeTypeandInlineQueryResultCachedSticker.Title: not part of the Bot API;NewInlineQueryResultCachedStickerno longer acceptstitle.- The
EphemeralSendParamsalias: useEphemeralMessageParameters. The field embedded in send configs is renamed with it:MessageConfig{EphemeralSendParams: …}becomesMessageConfig{EphemeralMessageParameters: …}andcfg.EphemeralSendParams.Xbecomescfg.EphemeralMessageParameters.X; promoted access such ascfg.ReceiverUserIDis unchanged. Params.AddInterface: useAddAny.
- Documentation:
- Runnable godoc examples on pkg.go.dev.
- The documentation book is published at https://bssth.github.io/telegram-bot-api/, with new pages for rich messages, ephemeral messages, poll media and business connections.
- Development:
- Integration tests read the bot token and target chats from environment
variables and skip cleanly without them. Once the test bot’s secrets are
configured, CI runs them on pushes to
master, daily and on demand. - The weekly Bot API spec check opens (or updates) a
bot-api-updateissue when a new Bot API version adds something the library lacks. specdiffgained-format text|markdown|json.
- Integration tests read the bot token and target chats from environment
variables and skip cleanly without them. Once the test bot’s secrets are
configured, CI runs them on pushes to
v6.0.0
- Module path is now
github.com/bssth/telegram-bot-api/v6: the upstream repository is no longer maintained, and the new major version reflects the source-incompatible changes listed in BREAKING.md. - Support every Bot API version from 6.1 to 10.3, based on the unmerged upstream pull request #794.
- Audit against the machine-readable Bot API 10.3 specification: all types, fields, methods and parameters are present.
- Upload files nested in polls and rich messages via
attach://. - Redact the bot token from transport errors.
- Add
internal/cmd/specdiffand a weekly workflow that report gaps against the latest Bot API specification. - Require Go 1.24.
- See BREAKING.md for source-incompatible changes.
v5.4.0
- Remove all methods that return
(APIResponse, error).- Use the
Requestmethod instead. - For more information, see Library Structure.
- Use the
- Remove all
New*UploadandNew*Sharemethods, replace withNew*.- Use different file types to specify if upload or share.
- Rename
UploadFiletoUploadFiles, accept[]RequestFileinstead of a single fieldname and file. - Fix methods returning
APIResponseand errors to always use pointers. - Update user IDs to
int64because of Bot API changes. - Add missing Bot API features.
Internals
If you want to contribute to the project, here’s some more information about the internal structure of the library.
Adding Endpoints
This is mostly useful if you’ve managed to catch a new Telegram Bot API update before the library can get updated. It’s also a great source of information about how the types work internally.
Creating the Config
The first step in adding a new endpoint is to create a new Config type for it.
These belong in configs.go.
Let’s try and add the deleteMessage endpoint. We can see it requires two
fields; chat_id and message_id. We can create a struct for these.
type DeleteMessageConfig struct {
ChatID ???
MessageID int
}
What type should ChatID be? Telegram allows specifying numeric chat IDs or
channel usernames. Golang doesn’t have union types, and interfaces are entirely
untyped. This library solves this by adding two fields, a ChatID and a
ChannelUsername. We can now write the struct as follows.
type DeleteMessageConfig struct {
ChannelUsername string
ChatID int64
MessageID int
}
Note that ChatID is an int64. Telegram chat IDs can be greater than 32 bits.
Okay, we now have our struct. But we can’t send it yet. It doesn’t implement
Chattable so it won’t work with Request or Send.
Making it Chattable
We can see that Chattable only requires a few methods.
type Chattable interface {
params() (Params, error)
method() string
}
params is the fields associated with the request. method is the endpoint
that this Config is associated with.
Implementing the method is easy, so let’s start with that.
func (config DeleteMessageConfig) method() string {
return "deleteMessage"
}
Now we have to add the params. The Params type is an alias for
map[string]string. Telegram expects only a single field for chat_id, so we
have to determine what data to send.
We could use an if statement to determine which field to get the value from.
However, as this is a relatively common operation, there’s helper methods for
Params. We can use the AddFirstValid method to go through each possible
value and stop when it discovers a valid one. Before writing your own Config,
it’s worth taking a look through params.go to see what other helpers exist.
Now we can take a look at what a completed params method looks like.
func (config DeleteMessageConfig) params() (Params, error) {
params := make(Params)
params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername)
params.AddNonZero("message_id", config.MessageID)
return params, nil
}
Uploading Files
Let’s imagine that for some reason deleting a message requires a document to be
uploaded and an optional thumbnail for that document. To add file upload
support we need to implement Fileable. This only requires one additional
method.
type Fileable interface {
Chattable
files() []RequestFile
}
First, let’s add some fields to store our files in. Most of the standard Configs have similar fields for their files.
type DeleteMessageConfig struct {
ChannelUsername string
ChatID int64
MessageID int
+ Delete RequestFileData
+ Thumbnail RequestFileData
}
Adding another method is pretty simple. We’ll always add a file named delete
and add the thumbnail file if we have one.
func (config DeleteMessageConfig) files() []RequestFile {
files := []RequestFile{{
Name: "delete",
Data: config.Delete,
}}
if config.Thumbnail != nil {
files = append(files, RequestFile{
Name: "thumbnail",
Data: config.Thumbnail,
})
}
return files
}
And now our files will upload! It will transparently handle uploads whether File
is a FilePath, FileURL, FileBytes, FileReader, or FileID.
Base Configs
Certain Configs have repeated elements. For example, many of the items sent to a
chat have ChatID or ChannelUsername fields, along with ReplyParameters,
ReplyMarkup, and DisableNotification. Instead of implementing all of this
code for each item, there’s a BaseChat that handles it for your Config.
Simply embed it in your struct to get all of those fields.
There’s only a few fields required for the MessageConfig struct after
embedding the BaseChat struct.
type MessageConfig struct {
BaseChat
Text string
ParseMode string
Entities []MessageEntity
LinkPreviewOptions *LinkPreviewOptions
}
It also inherits the params method from BaseChat. This allows you to call
it, then you only have to add your new fields.
func (config MessageConfig) params() (Params, error) {
params, err := config.BaseChat.params()
if err != nil {
return params, err
}
params.AddNonEmpty("text", config.Text)
// Add your other fields
return params, nil
}
Similarly, there’s a BaseFile struct for adding an associated file and
BaseEdit struct for editing messages.
Making it Friendly
After we’ve got a Config type, we’ll want to make it more user-friendly. We can
do this by adding a new helper to helpers.go. These are functions that take
in the required data for the request to succeed and populate a Config.
Telegram only requires two fields to call deleteMessage, so this will be fast.
func NewDeleteMessage(chatID int64, messageID int) DeleteMessageConfig {
return DeleteMessageConfig{
ChatID: chatID,
MessageID: messageID,
}
}
Sometimes it makes sense to add more helpers if there’s methods where you have
to set exactly one field. You can also add helpers that accept a username
string for channels if it’s a common operation.
And that’s it! You’ve added a new method.
Uploading Files
To make files work as expected, there’s a lot going on behind the scenes. Make sure to read through the Files section in Getting Started first as we’ll be building on that information.
This section only talks about file uploading. For non-uploaded files such as URLs and file IDs, you just need to pass a string.
Fields
Let’s start by talking about how the library represents files as part of a Config.
Static Fields
Most endpoints use static file fields. For example, sendPhoto expects a single
file named photo. All we have to do is set that single field with the correct
value (either a string or multipart file). Methods like sendDocument take two
file uploads, a document and a thumbnail. These are pretty straightforward.
Remembering that the Fileable interface only requires one method, let’s
implement it for DocumentConfig.
func (config DocumentConfig) files() []RequestFile {
// We can have multiple files, so we'll create an array. We also know that
// there always is a document file, so initialize the array with that.
files := []RequestFile{{
Name: "document",
Data: config.File,
}}
// We'll only add a file if we have one.
if config.Thumbnail != nil {
files = append(files, RequestFile{
Name: "thumbnail",
Data: config.Thumbnail,
})
}
return files
}
Telegram also supports the attach:// syntax (discussed more later) for
thumbnails, but there’s no reason to make things more complicated.
Dynamic Fields
Of course, not everything can be so simple. Methods like sendMediaGroup
can accept many files, and each file can have custom markup. Using a static
field isn’t possible because we need to specify which field is attached to each
item. Telegram introduced the attach:// syntax for this.
Let’s follow through creating a new media group with string and file uploads.
First, we start by creating some InputMediaPhoto.
photo := tgbotapi.NewInputMediaPhoto(tgbotapi.FilePath("tests/image.jpg"))
url := tgbotapi.NewInputMediaPhoto(tgbotapi.FileURL("https://i.imgur.com/unQLJIb.jpg"))
This created a new InputMediaPhoto struct, with a type of photo and the
media interface that we specified.
We’ll now create our media group with the photo and URL.
mediaGroup := NewMediaGroup(ChatID, []interface{}{
photo,
url,
})
A MediaGroupConfig stores all the media in an array of interfaces. We now
have all the data we need to upload, but how do we figure out field names for
uploads? We didn’t specify attach://unique-file anywhere.
When the library goes to upload the files, it looks at the params and files
for the Config. The params are generated by transforming the file into a value
more suitable for uploading, file IDs and URLs are untouched but uploaded types
are all changed into attach://file-%d. When collecting a list of files to
upload, it names them the same way. This creates a nearly transparent way of
handling multiple files in the background without the user having to consider
what’s going on.
Nested Media
Newer methods accept media nested inside JSON-serialized parameters: a poll can
carry media for its question, each option and the quiz explanation, and a rich
message can embed photos, videos, documents and so on in its media list and
in its blocks. Telegram resolves attach:// references anywhere in the request,
so these work the same way as media groups.
SendPollConfig, SendRichMessageConfig, SendRichMessageDraftConfig,
EditMessageTextConfig and EditEphemeralMessageTextConfig walk their nested
media with a nestedMediaUploader (see nested_media.go). Every file that
needs uploading is replaced by attach://poll-media-%d or
attach://rich-media-%d in a copy of the media, and the matching
RequestFile is collected for files(). Because params() and files()
walk the media in the same order, the names always line up, and the caller’s
config is never modified.
poll := tgbotapi.NewPoll(chatID, "Which one?", "Left", "Right")
poll.Options[0].Media = tgbotapi.NewInputMediaPhoto(tgbotapi.FilePath("left.jpg"))
poll.Options[1].Media = tgbotapi.NewInputMediaPhoto(tgbotapi.FileID(rightFileID))
bot.Send(poll)