Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

TypeDescription
FilePathA local path to a file
FileIDExisting file ID on Telegram’s servers
FileURLURL to file, must be served with expected MIME type
FileReaderUse 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 ChatMember updates, you must explicitly add UpdateTypeChatMember to your AllowedUpdates when 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 Update will 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 int64 types 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![Screenshot](tg://photo?id=shot1)\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.Context support:
    • MakeRequestWithContext, UploadFilesWithContext, RequestWithContext and SendWithContext. The existing methods call them with context.Background().
    • GetUpdatesWithContext, GetMeWithContext, GetFileWithContext and SendMediaGroupWithContext.
    • Generic RequestTyped[T] and RequestTypedWithContext[T], which decode a request’s result into T.
    • 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.
  • StopReceivingUpdates is idempotent and polling can be restarted on the same BotAPI. It now stops only pollers that are already running: calling it before GetUpdatesChan no longer makes that call return a closed channel. Use GetUpdatesChanWithContext for race-free shutdown, and set UpdateConfig.Offset to the last update ID plus one when restarting.
  • Add optional automatic retry on 429 Too Many Requests:
    • BotAPI.MaxRetries (default 0, disabled) retries after the retry_after Telegram asks for. The wait honours the request context.
    • BotAPI.MaxRetryAfter limits how long the bot is willing to wait; a longer retry_after returns the 429 error immediately.
    • Uploads are retried only when every uploaded file is a FileBytes or a FilePath.
    • IsTooManyRequests(err) reports whether an error is a 429 and returns its retry_after.
  • Verify the X-Telegram-Bot-Api-Secret-Token header of webhook requests:
    • Set BotAPI.WebhookSecretToken to the SecretToken passed to setWebhook. HandleUpdate then returns ErrBadSecretToken, and ListenForWebhook / ListenForWebhookRespReqFormat answer 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/json header.
  • Remove deprecated methods and fields:
    • BotAPI.GetChatMembersCount: use GetChatMemberCount.
    • BanChatSenderChatConfig.UntilDate: no longer part of banChatSenderChat.
    • InlineQueryResultPhoto.MimeType and InlineQueryResultCachedSticker.Title: not part of the Bot API; NewInlineQueryResultCachedSticker no longer accepts title.
    • The EphemeralSendParams alias: use EphemeralMessageParameters. The field embedded in send configs is renamed with it: MessageConfig{EphemeralSendParams: …} becomes MessageConfig{EphemeralMessageParameters: …} and cfg.EphemeralSendParams.X becomes cfg.EphemeralMessageParameters.X; promoted access such as cfg.ReceiverUserID is unchanged.
    • Params.AddInterface: use AddAny.
  • 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-update issue when a new Bot API version adds something the library lacks.
    • specdiff gained -format text|markdown|json.

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/specdiff and 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).
  • Remove all New*Upload and New*Share methods, replace with New*.
    • Use different file types to specify if upload or share.
  • Rename UploadFile to UploadFiles, accept []RequestFile instead of a single fieldname and file.
  • Fix methods returning APIResponse and errors to always use pointers.
  • Update user IDs to int64 because 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)