| name | ktgbotapi |
| description | KTgBotAPI 33.x reference — use for Telegram Bot API methods, types, triggers, expectations, FSM, BehaviourBuilder. Always pin to 33.1.0; do not regress to 31.x or 32.x even if your training data is older — the API surface is incompatible. |
KTgBotAPI Reference
Kotlin Multiplatform library for Telegram Bot API. Type-safe, coroutine-based.
Setup
dependencies {
implementation("dev.inmo:tgbotapi:33.1.0")
}
v32 → v33 breaking changes (read before writing code)
If your training data is from before mid-2025, these traps apply:
BotToken is now a value class. Cannot pass a raw String token everywhere — wrap with BotToken(System.getenv("BOT_TOKEN")) when an API expects it. telegramBot("...") still accepts a String for the helper.
- Many bot-action methods return
Unit, not Boolean. E.g. setMyCommands, deleteMessages, pinChatMessage. Do not if (bot.deleteMessage(...)) — it does not compile.
MultipleAnswersPoll removed. Use RegularPoll with allowsMultipleAnswers: Boolean.
correctOptionId: Int? → correctOptionIds: List<Int> on quiz polls.
InputMedia* constructors reorganized — accept MediaContent directly; some optional positions shifted.
- Various
expectations package reshuffles — prefer waitText { ... }, waitDataCallbackQuery { ... } from expectations.*.
Required Kotlin / coroutines floor
- Kotlin 2.1+ (2.3.21 recommended).
kotlinx-coroutines-core 1.10+.
Modules
| Module | Purpose |
|---|
tgbotapi.core | Core types, requests |
tgbotapi.api | API extension methods |
tgbotapi.utils | Utilities, keyboard builders |
tgbotapi.behaviour_builder | BehaviourBuilder DSL |
tgbotapi.behaviour_builder.fsm | FSM integration |
Quick Start
suspend fun main() {
val bot = telegramBot(System.getenv("BOT_TOKEN"))
bot.buildBehaviourWithLongPolling {
onCommand("start") { reply(it, "Hello!") }
}.join()
}
Triggers Reference
Commands
CRITICAL: Understanding requireOnlyCommandInMessage parameter
By default, onCommand has requireOnlyCommandInMessage = true, meaning it ONLY triggers when the message contains JUST the command with no additional text.
onCommand("start") { message -> }
For commands with arguments, you MUST use one of these approaches:
onCommand("mute", requireOnlyCommandInMessage = false) { message ->
val text = (message.content as TextContent).text
val args = text.split(" ").drop(1)
}
onCommandWithArgs("echo") { message, args ->
reply(message, args.joinToString(" "))
}
onCommandWithNamedArgs("config") { message, args ->
}
Common patterns:
onCommand("start") { message -> }
onCommand("help", "info") { message -> }
onCommand(Regex("set_.*")) { message -> }
onCommand("warn", requireOnlyCommandInMessage = false) { }
onCommandWithArgs("echo") { message, args -> }
onDeepLink { message, deepLink -> }
onUnhandledCommand { message -> }
Text
onText { message -> }
onText(initialFilter = { it.content.text.length > 10 }) { message -> }
onText(Regex("\\d+")) { message -> }
Media
onPhoto { message -> }
onVideo { message -> }
onAudio { message -> }
onDocument { message -> }
onVoice { message -> }
onVideoNote { message -> }
onSticker { message -> }
onAnimation { message -> }
onMediaGroup { messages -> }
onVisualMediaGroup { messages -> }
Callbacks & Queries
onDataCallbackQuery { query -> }
onDataCallbackQuery(Regex("action:.*")) { query -> }
onInlineQuery { query -> }
onChosenInlineResult { result -> }
Other Updates
onContact { message -> }
onLocation { message -> }
onPoll { poll -> }
onPollAnswer { answer -> }
onChatMemberUpdated { update -> }
onMyChatMemberUpdated { update -> }
onNewChatMembers { message -> }
onLeftChatMember { message -> }
Expectations Reference
Wait for specific user input:
val text = waitText().first()
val text = waitText { it.chat.id == chatId }.first()
val photo = waitPhoto().first()
val document = waitDocument().first()
val callback = waitDataCallbackQuery().first()
val callback = waitDataCallbackQuery { it.data.startsWith("confirm:") }.first()
val photo = waitPhoto(
SendTextMessage(chatId, "Send me a photo")
).first()
Sending Messages
sendMessage(chatId, "Hello")
sendTextMessage(chatId, "Hello", parseMode = ParseMode.HTML)
reply(message, "Reply text")
send(chatId, buildEntities {
bold("Bold") + " and " + italic("italic")
})
sendPhoto(chatId, InputFile.fromFile(File("photo.jpg")))
sendPhoto(chatId, InputFile.fromUrl("https://..."))
sendDocument(chatId, InputFile.fromFile(File("doc.pdf")))
sendVideo(chatId, InputFile.fromFile(File("video.mp4")))
sendAudio(chatId, InputFile.fromFile(File("audio.mp3")))
sendVoice(chatId, InputFile.fromFile(File("voice.ogg")))
sendSticker(chatId, InputFile.fromId(stickerFileId))
sendMediaGroup(chatId, listOf(
TelegramMediaPhoto(InputFile.fromFile(File("1.jpg"))),
TelegramMediaPhoto(InputFile.fromFile(File("2.jpg")))
))
sendLocation(chatId, latitude = 55.75, longitude = 37.62)
sendContact(chatId, phoneNumber = "+123456789", firstName = "John")
Text Formatting
buildEntities DSL
val text = buildEntities {
bold("Bold") + "\n"
italic("Italic") + "\n"
underline("Underline") + "\n"
strikethrough("Strike") + "\n"
spoiler("Spoiler") + "\n"
code("inline code") + "\n"
pre("code block", language = "kotlin")
link("Link", "https://example.com") + "\n"
mention("username")
textMention("User", userId)
botCommand("start")
hashtag("tag")
cashtag("USD")
email("test@example.com")
phoneNumber("+123456789")
regular("Plain text")
}
Parse Modes
sendTextMessage(chatId, """
<b>Bold</b>, <i>italic</i>, <u>underline</u>
<s>strikethrough</s>, <tg-spoiler>spoiler</tg-spoiler>
<code>code</code>, <pre>block</pre>
<a href="https://...">link</a>
""".trimIndent(), parseMode = ParseMode.HTML)
sendTextMessage(chatId, """
*bold*, _italic_, __underline__
~strikethrough~, ||spoiler||
`code`, ```block```
[link](https://...)
""".trimIndent(), parseMode = ParseMode.MarkdownV2)
Reply Keyboard
val keyboard = replyKeyboard(
resizeKeyboard = true,
oneTimeKeyboard = true,
inputFieldPlaceholder = "Choose option"
) {
row {
simpleButton("Button 1")
simpleButton("Button 2")
}
row {
requestContactButton("Share Contact")
requestLocationButton("Share Location")
}
row {
requestPollButton("Create Poll", type = RegularPoll)
webAppButton("Web App", WebAppInfo("https://..."))
}
}
sendMessage(chatId, "Menu:", replyMarkup = keyboard)
sendMessage(chatId, "Done", replyMarkup = ReplyKeyboardRemove())
Inline Keyboard
val keyboard = inlineKeyboard {
row {
dataButton("Action", "callback_data")
urlButton("Link", "https://example.com")
}
row {
webAppButton("Web App", WebAppInfo("https://..."))
loginButton("Login", LoginUrl("https://..."))
}
row {
inlineQueryInCurrentChatButton("Search", "query")
inlineQueryInChosenChatButton("Search chat", "query")
}
row {
payButton("Pay")
gameButton("Play")
}
}
sendMessage(chatId, "Actions:", replyMarkup = keyboard)
Callback Queries
onDataCallbackQuery { query ->
val data = query.data
val message = query.message
val user = query.user
answer(query)
answer(query, "Notification text")
answer(query, "Alert!", showAlert = true)
edit(query.message!!, "New text")
edit(query.message!!, replyMarkup = newKeyboard)
delete(query.message!!)
}
Inline Mode
onInlineQuery { query ->
val searchQuery = query.query
val results = listOf(
InlineQueryResultArticle(
id = "1",
title = "Result Title",
description = "Description",
inputMessageContent = InputTextMessageContent("Selected: Result 1"),
replyMarkup = inlineKeyboard { row { dataButton("Details", "d:1") } }
),
InlineQueryResultPhoto(
id = "2",
photoUrl = "https://example.com/photo.jpg",
thumbnailUrl = "https://example.com/thumb.jpg"
),
InlineQueryResultGif(
id = "3",
gifUrl = "https://example.com/animation.gif",
thumbnailUrl = "https://example.com/thumb.gif"
)
)
answerInlineQuery(
query,
results,
cacheTime = 300,
isPersonal = false,
button = InlineQueryResultsButton("Open Bot", StartParameter("param"))
)
}
onChosenInlineResult { result ->
val resultId = result.resultId
val query = result.query
}
FSM (Finite State Machine)
FSM imports — easy 2 mis-import; pin these 4 ktgbotapi 33.1.0:
import dev.inmo.tgbotapi.extensions.behaviour_builder.fsm.*
import dev.inmo.tgbotapi.extensions.behaviour_builder.fsm.strictlyOn
import dev.inmo.micro_utils.fsm.common.State
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitText
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithFSMAndStartLongPolling
Notes:
State lives in micro_utils.fsm.common (NOT tgbotapi.*) — separate artifact dev.inmo:micro_utils.fsm.common.
strictlyOn + FSM context builders live under extensions.behaviour_builder.fsm (artifact tgbotapi.behaviour_builder.fsm).
waitText / other wait* expectations live under extensions.behaviour_builder.expectations, NOT under .fsm.
buildBehaviourWithFSMAndStartLongPolling sits directly in extensions.behaviour_builder (parent package), not .fsm.
sealed interface BotState : State {
override val context: IdChatIdentifier
data class WaitingName(override val context: IdChatIdentifier) : BotState
data class WaitingAge(override val context: IdChatIdentifier, val name: String) : BotState
}
bot.buildBehaviourWithFSMAndStartLongPolling<BotState> {
onCommand("start") { message ->
startChain(BotState.WaitingName(message.chat.id))
}
strictlyOn<BotState.WaitingName> { state ->
send(state.context, "Enter your name:")
val name = waitText { it.chat.id == state.context }.first().content.text
BotState.WaitingAge(state.context, name)
}
strictlyOn<BotState.WaitingAge> { state ->
send(state.context, "Enter your age:")
val age = waitText { it.chat.id == state.context }.first().content.text
send(state.context, "Name: ${state.name}, Age: $age")
null
}
}.second.join()
Chat Management
val chat = getChat(chatId)
val member = getChatMember(chatId, userId)
banChatMember(chatId, userId)
banChatMember(chatId, userId, untilDate = Instant.now() + 1.hours)
unbanChatMember(chatId, userId)
restrictChatMember(chatId, userId, ChatPermissions(
canSendMessages = false,
canSendMediaMessages = false
))
promoteChatMember(chatId, userId,
canDeleteMessages = true,
canPinMessages = true
)
setChatAdministratorCustomTitle(chatId, userId, "Custom Title")
File Operations
val file = bot.downloadFile(fileId)
val bytes = bot.downloadFileToByteArray(fileId)
val fileInfo = getFile(fileId)
val filePath = fileInfo.filePath
Bot Settings
val me = getMe()
setMyCommands(listOf(
BotCommand("start", "Start the bot"),
BotCommand("help", "Show help")
))
setMyCommands(
commands = listOf(BotCommand("admin", "Admin panel")),
scope = BotCommandScopeChat(adminChatId)
)
deleteMyCommands()
setMyDescription("Bot description")
setMyShortDescription("Short description")
Error Handling
bot.buildBehaviourWithLongPolling(
defaultExceptionsHandler = { throwable ->
when (throwable) {
is CommonBotException -> println("API error: ${throwable.message}")
is CancellationException -> { }
else -> throwable.printStackTrace()
}
}
) {
}
Types Quick Reference
| Type | Description |
|---|
ChatId | Chat identifier (Long wrapper) |
UserId | User identifier |
MessageId | Message identifier |
FileId | File identifier |
IdChatIdentifier | Union of ChatId/UserId |
CommonMessage<T> | Message with content type T |
TextContent | Text message content |
PhotoContent | Photo message content |
DocumentContent | Document message content |
PrivateChat | Private chat type |
GroupChat | Group chat type |
SupergroupChat | Supergroup chat type |
ChannelChat | Channel chat type |