You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Go to file
Demian 71ac2995cc errors: implement ErrIs 3 weeks ago
.github/workflows actions: add secrets 2 years ago
layout Update layout.go 7 months ago
middleware middleware: implement Recover, add docs 1 year ago
.gitignore examples: add awslambdaechobot 4 years ago
LICENSE Initial commit 9 years ago README: getting rid of past import path artifacts 2 years ago
admin.go admin: add use_independent_chat_permissions param 3 weeks ago
admin_test.go admin: add use_independent_chat_permissions param 3 weeks ago
api.go api: always specify allowed_updates in getUpdates 9 months ago
api_test.go telebot: restructure and refactor 1 year ago
bot.go bot: describe how to include the caption in SendAlbum docs 3 weeks ago
bot_test.go tests: refactor middleware test cases 1 month ago
callback.go telebot: restructure and refactor 1 year ago
chat.go chat: add user_chat_id field 3 weeks ago
chat_test.go chat: create ChatID Recipient type 4 years ago
commands.go telebot: restructure and refactor 1 year ago
context.go topic: finish and refactor the implementation 3 weeks ago
context_test.go context: clarify 2 years ago
editable.go telebot: fix linter inspection problems 4 years ago
errors.go errors: implement ErrIs 3 weeks ago
file.go file: change FileSize type to int64 1 year ago
file_test.go tests: implement more 4 years ago
game.go telebot: restructure and refactor 1 year ago
go.mod layout: migrate from cast to the viper 1 year ago
go.sum layout: migrate from cast to the viper 1 year ago
inline.go MarshalJSON should produce unique result ID 3 weeks ago
inline_types.go options: remove InlineKeyboardMarkup type 2 years ago
input_types.go telebot: clarify and fix all naming issues 4 years ago
markup.go markup: add reply user and chat Btn constructors 3 weeks ago
markup_test.go markup: add WebApp to the Btn 1 year ago
media.go media: remove omitempty flag from json tag in Sticker 3 weeks ago
message.go topic: finish and refactor the implementation 3 weeks ago
middleware.go middleware: move m appending into separate func 1 month ago
options.go api: update to 6.4 (#606) 3 weeks ago
payments.go telebot: restructure and refactor 1 year ago
payments_data.go telebot: restructure and refactor 1 year ago
poll.go telebot: restructure and refactor 1 year ago
poll_test.go telebot: restructure and refactor 1 year ago
poller.go telebot: restructure and refactor 1 year ago
poller_test.go revert "poller: remove deprecated MiddlewarePoller" 2 years ago
sendable.go api: update to 6.3 3 weeks ago
stickers.go telebot: restructure and refactor 1 year ago
telebot.go api 6.5: implement request_user and request_chat support params in ReplyButton 3 weeks ago
topic.go topic: finish and refactor the implementation 3 weeks ago
update.go topic: finish and refactor the implementation 3 weeks ago
video_chat.go topic: finish and refactor the implementation 3 weeks ago
web_app.go api: update to 6.4 (#606) 3 weeks ago
webhook.go Make SetWebhook error loggable by default (#559) 1 year ago


"I never knew creating Telegram bots could be so sexy!"

GoDoc GitHub Actions Discuss on Telegram

go get -u


Telebot is a bot framework for Telegram Bot API. This package provides the best of its kind API for command routing, inline query requests and keyboards, as well as callbacks. Actually, I went a couple steps further, so instead of making a 1:1 API wrapper I chose to focus on the beauty of API and performance. Some strong sides of Telebot are:

  • Real concise API
  • Command routing
  • Middleware
  • Transparent File API
  • Effortless bot callbacks

All the methods of Telebot API are extremely easy to memorize and get used to. Also, consider Telebot a highload-ready solution. I'll test and benchmark the most popular actions and if necessary, optimize against them without sacrificing API quality.

Getting Started

Let's take a look at the minimal Telebot setup:

package main

import (

	tele ""

func main() {
	pref := tele.Settings{
		Token:  os.Getenv("TOKEN"),
		Poller: &tele.LongPoller{Timeout: 10 * time.Second},

	b, err := tele.NewBot(pref)
	if err != nil {

	b.Handle("/hello", func(c tele.Context) error {
		return c.Send("Hello!")


Simple, innit? Telebot's routing system takes care of delivering updates to their endpoints, so in order to get to handle any meaningful event, all you got to do is just plug your function into one of the Telebot-provided endpoints. You can find the full list here.

There are dozens of supported endpoints (see package consts). Let me know if you'd like to see some endpoint or endpoint ideas implemented. This system is completely extensible, so I can introduce them without breaking backwards compatibility.


Context is a special type that wraps a huge update structure and represents the context of the current event. It provides several helpers, which allow getting, for example, the chat that this update had been sent in, no matter what kind of update this is.

b.Handle(tele.OnText, func(c tele.Context) error {
	// All the text messages that weren't
	// captured by existing handlers.

	var (
		user = c.Sender()
		text = c.Text()

	// Use full-fledged bot's functions
	// only if you need a result:
	msg, err := b.Send(user, text)
	if err != nil {
		return err

	// Instead, prefer a context short-hand:
	return c.Send(text)

b.Handle(tele.OnChannelPost, func(c tele.Context) error {
	// Channel posts only.
	msg := c.Message()

b.Handle(tele.OnPhoto, func(c tele.Context) error {
	// Photos only.
	photo := c.Message().Photo

b.Handle(tele.OnQuery, func(c tele.Context) error {
	// Incoming inline queries.
	return c.Answer(...)


Telebot has a simple and recognizable way to set up middleware — chained functions with access to Context, called before the handler execution.

Import a middleware package to get some basic out-of-box middleware implementations:

import ""
// Global-scoped middleware:

// Group-scoped middleware:
adminOnly := b.Group()
adminOnly.Handle("/ban", onBan)
adminOnly.Handle("/kick", onKick)

// Handler-scoped middleware:
b.Handle(tele.OnText, onText, middleware.IgnoreVia())

Custom middleware example:

// AutoResponder automatically responds to every callback update.
func AutoResponder(next tele.HandlerFunc) tele.HandlerFunc {
	return func(c tele.Context) error {
		if c.Callback() != nil {
			defer c.Respond()
		return next(c) // continue execution chain


Telebot doesn't really care how you provide it with incoming updates, as long as you set it up with a Poller, or call ProcessUpdate for each update:

// Poller is a provider of Updates.
// All pollers must implement Poll(), which accepts bot
// pointer and subscription channel and start polling
// synchronously straight away.
type Poller interface {
	// Poll is supposed to take the bot object
	// subscription channel and start polling
	// for Updates immediately.
	// Poller must listen for stop constantly and close
	// it as soon as it's done polling.
	Poll(b *Bot, updates chan Update, stop chan struct{})


When handling commands, Telebot supports both direct (/command) and group-like syntax (/command@botname) and will never deliver messages addressed to some other bot, even if privacy mode is off.

For simplified deep-linking, Telebot also extracts payload:

// Command: /start <PAYLOAD>
b.Handle("/start", func(c tele.Context) error {
	fmt.Println(c.Message().Payload) // <PAYLOAD>

For multiple arguments use:

// Command: /tags <tag1> <tag2> <...>
b.Handle("/tags", func(c tele.Context) error {
	tags := c.Args() // list of arguments splitted by a space
	for _, tag := range tags {
		// iterate through passed arguments


Telegram allows files up to 50 MB in size.

Telebot allows to both upload (from disk or by URL) and download (from Telegram) files in bot's scope. Also, sending any kind of media with a File created from disk will upload the file to Telegram automatically:

a := &tele.Audio{File: tele.FromDisk("file.ogg")}

fmt.Println(a.OnDisk()) // true
fmt.Println(a.InCloud()) // false

// Will upload the file from disk and send it to the recipient
b.Send(recipient, a)

// Next time you'll be sending this very *Audio, Telebot won't
// re-upload the same file but rather utilize its Telegram FileID
b.Send(otherRecipient, a)

fmt.Println(a.OnDisk()) // true
fmt.Println(a.InCloud()) // true
fmt.Println(a.FileID) // <Telegram file ID>

You might want to save certain Files in order to avoid re-uploading. Feel free to marshal them into whatever format, File only contain public fields, so no data will ever be lost.


Send is undoubtedly the most important method in Telebot. Send() accepts a Recipient (could be user, group or a channel) and a Sendable. Other types other than the Telebot-provided media types (Photo, Audio, Video, etc.) are Sendable. If you create composite types of your own, and they satisfy the Sendable interface, Telebot will be able to send them out.

// Sendable is any object that can send itself.
// This is pretty cool, since it lets bots implement
// custom Sendables for complex kinds of media or
// chat objects spanning across multiple messages.
type Sendable interface {
	Send(*Bot, Recipient, *SendOptions) (*Message, error)

The only type at the time that doesn't fit Send() is Album and there is a reason for that. Albums were added not so long ago, so they are slightly quirky for backwards compatibilities sake. In fact, an Album can be sent, but never received. Instead, Telegram returns a []Message, one for each media object in the album:

p := &tele.Photo{File: tele.FromDisk("chicken.jpg")}
v := &tele.Video{File: tele.FromURL("http://video.mp4")}

msgs, err := b.SendAlbum(user, tele.Album{p, v})

Send options

Send options are objects and flags you can pass to Send(), Edit() and friends as optional arguments (following the recipient and the text/media). The most important one is called SendOptions, it lets you control all the properties of the message supported by Telegram. The only drawback is that it's rather inconvenient to use at times, so Send() supports multiple shorthands:

// regular send options
b.Send(user, "text", &tele.SendOptions{
	// ...

// ReplyMarkup is a part of SendOptions,
// but often it's the only option you need
b.Send(user, "text", &tele.ReplyMarkup{
	// ...

// flags: no notification && no web link preview
b.Send(user, "text", tele.Silent, tele.NoPreview)

Full list of supported option-flags you can find here.


If you want to edit some existing message, you don't really need to store the original *Message object. In fact, upon edit, Telegram only requires chat_id and message_id. So you don't really need the Message as a whole. Also, you might want to store references to certain messages in the database, so I thought it made sense for any Go struct to be editable as a Telegram message, to implement Editable:

// Editable is an interface for all objects that
// provide "message signature", a pair of 32-bit
// message ID and 64-bit chat ID, both required
// for edit operations.
// Use case: DB model struct for messages to-be
// edited with, say two columns: msg_id,chat_id
// could easily implement MessageSig() making
// instances of stored messages editable.
type Editable interface {
	// MessageSig is a "message signature".
	// For inline messages, return chatID = 0.
	MessageSig() (messageID int, chatID int64)

For example, Message type is Editable. Here is the implementation of StoredMessage type, provided by Telebot:

// StoredMessage is an example struct suitable for being
// stored in the database as-is or being embedded into
// a larger struct, which is often the case (you might
// want to store some metadata alongside, or might not.)
type StoredMessage struct {
	MessageID int   `sql:"message_id" json:"message_id"`
	ChatID    int64 `sql:"chat_id" json:"chat_id"`

func (x StoredMessage) MessageSig() (int, int64) {
	return x.MessageID, x.ChatID

Why bother at all? Well, it allows you to do things like this:

// just two integer columns in the database
var msgs []tele.StoredMessage
db.Find(&msgs) // gorm syntax

for _, msg := range msgs {
	bot.Edit(&msg, "Updated text")
	// or

I find it incredibly neat. Worth noting, at this point of time there exists another method in the Edit family, EditCaption() which is of a pretty rare use, so I didn't bother including it to Edit(), just like I did with SendAlbum() as it would inevitably lead to unnecessary complications.

var m *Message

// change caption of a photo, audio, etc.
bot.EditCaption(m, "new caption")


Telebot supports both kinds of keyboards Telegram provides: reply and inline keyboards. Any button can also act as endpoints for Handle().

var (
	// Universal markup builders.
	menu     = &tele.ReplyMarkup{ResizeKeyboard: true}
	selector = &tele.ReplyMarkup{}

	// Reply buttons.
	btnHelp     = menu.Text(" Help")
	btnSettings = menu.Text("⚙ Settings")

	// Inline buttons.
	// Pressing it will cause the client to
	// send the bot a callback.
	// Make sure Unique stays unique as per button kind
	// since it's required for callback routing to work.
	btnPrev = selector.Data("⬅", "prev", ...)
	btnNext = selector.Data("➡", "next", ...)

	selector.Row(btnPrev, btnNext),

b.Handle("/start", func(c tele.Context) error {
	return c.Send("Hello!", menu)

// On reply button pressed (message)
b.Handle(&btnHelp, func(c tele.Context) error {
	return c.Edit("Here is some help: ...")

// On inline button pressed (callback)
b.Handle(&btnPrev, func(c tele.Context) error {
	return c.Respond()

You can use markup constructor for every type of possible button:

r := b.NewMarkup()

// Reply buttons:
r.Contact("Send phone number")
r.Location("Send location")

// Inline buttons:
r.Data("Show help", "help") // data is optional
r.Data("Delete item", "delete", item.ID)
r.URL("Visit", "")
r.Query("Search", query)
r.QueryChat("Share", query)
r.Login("Login", &tele.Login{...})

Inline mode

So if you want to handle incoming inline queries you better plug the tele.OnQuery endpoint and then use the Answer() method to send a list of inline queries back. I think at the time of writing, Telebot supports all of the provided result types (but not the cached ones). This is what it looks like:

b.Handle(tele.OnQuery, func(c tele.Context) error {
	urls := []string{

	results := make(tele.Results, len(urls)) // []tele.Result
	for i, url := range urls {
		result := &tele.PhotoResult{
			URL:      url,
			ThumbURL: url, // required for photos

		results[i] = result
		// needed to set a unique string ID for each result

	return c.Answer(&tele.QueryResponse{
		Results:   results,
		CacheTime: 60, // a minute

There's not much to talk about really. It also supports some form of authentication through deep-linking. For that, use fields SwitchPMText and SwitchPMParameter of QueryResponse.


  1. Fork it
  2. Clone v3: git clone -b v3
  3. Create your feature branch: git checkout -b v3-feature
  4. Make changes and add them: git add .
  5. Commit: git commit -m "add some feature"
  6. Push: git push origin v3-feature
  7. Pull request


I do coding for fun, but I also try to search for interesting solutions and optimize them as much as possible. If you feel like it's a good piece of software, I wouldn't mind a tip!

Litecoin: ltc1qskt5ltrtyg7esfjm0ftx6jnacwffhpzpqmerus

Ethereum: 0xB78A2Ac1D83a0aD0b993046F9fDEfC5e619efCAB


Telebot is distributed under MIT.