Bot API 10.4 (November 9, 2026)
Widgets
- Added
sendWidget,sendSharedWidget,answerWidgetQuery,editMessageWidgetData, andeditSharedWidgetData. - Added
WidgetandwidgetonMessageandExternalReplyInfo. - Added
WidgetQuery/SharedWidgetQueryand thewidget_query/shared_widget_queryupdate types, plusWidgetQueryType(WidgetQueryTypeData,WidgetQueryTypeSignData,WidgetQueryTypeSentTransaction) andWidgetQueryWallet. - Added
InputWidgetMessageContentandInputSharedWidgetMessageContent. ctx.chat/ctx.fromresolve for widget queries (shared_widget_queryhas no chat).
await bot.api.sendWidget({
chat_id: userId, // widgets work in private chats
allow_queries: true,
html: `<button id="b">Tap me</button>
<script>
b.onclick = () => TelegramWidget.request("tap").then((a) => (b.textContent = a.result));
</script>`,
});
bot.on("widget_query", async (ctx) => {
if (!("widget_query" in ctx.update)) return;
const q = ctx.update.widget_query;
await ctx.api.answerWidgetQuery({ widget_query_id: q.id, data: `hi ${ctx.from?.first_name}` });
});Ephemeral Messages
chat_idin the ephemeral-capable send methods andreceiver_user_idinEphemeralMessageParametersare now optional, socallback_query_idalone sends an ephemeral reply to a callback query (in any chat except channel direct-messages chats).- Types are stricter than the "Optional" columns in the Bot API docs, to catch at compile time what Telegram rejects at runtime:
- A call to one of these methods without
chat_idis a type error unless it is an ephemeral reply to a callback query (callback_query_idwithoutreceiver_user_id). The rule is the exportedChatOrCallbackReplytype. EphemeralMessageParametersmust havereceiver_user_idorcallback_query_id. The callback-reply form is the exportedEphemeralCallbackReplytype.OmitandPickmerge the members of these param types, so a wrapper such asOmit<SendMessageParams, "parse_mode">loses the rule and its spread is rejected. Use the exportedDistributiveOmit<SendMessageParams, "parse_mode">.Omit<SendMessageParams, "chat_id">plus your ownchat_idworks as before.interface X extends SendMessageParamsno longer compiles; use atypeintersection.
- A call to one of these methods without
ctx.replyon an update without a chat (e.g. a callback query from an inline message) sends whenothermakes it an ephemeral reply to the callback query, instead of throwing.
bot.on("callback_query", async (ctx) => {
const q = ctx.callbackQuery;
if (!q) return;
// No chat_id: shown only to the user who tapped, marked "Only visible to you"
await ctx.api.sendMessage({
text: "Only you can see this",
ephemeral_message_parameters: { callback_query_id: q.id },
});
await ctx.answerCallbackQuery(); // still required after an ephemeral reply
});
await bot.api.sendMessage({ text: "hi" }); // ✗ type error: chat_id is required here
// Wrapping the param types: use DistributiveOmit, not Omit
import type { DistributiveOmit, SendMessageParams } from "node-telegram-bot-api";
const sendHtml = (p: DistributiveOmit<SendMessageParams, "parse_mode">) =>
bot.api.sendMessage({ ...p, parse_mode: "HTML" });Quotes from rich messages
- Added
rich_textstoTextQuoteandquote_rich_textstoReplyParameters.
await bot.api.sendMessage({
chat_id: chatId,
text: "Agreed!",
reply_parameters: {
message_id: richMessageId,
quote_rich_texts: ["the exact sentence being quoted"],
},
});TON wallet
- Added
TonWalletCard,TonWalletCardSticker,TonNft,TonNftAttribute, andChatFullInfo.ton_wallet_card. - Added
TonWalletTransferandMessage.ton_wallet_transfer. - Renamed
StoryAreaPositiontoAreaPosition, following the Bot API.StoryAreaPositionstays as a deprecated alias.
Chat share themes
- Added
ChatShareTheme(ChatShareThemeEmoji,ChatShareThemeUniqueGift),ChatFullInfo.share_theme, andEmojiType(EmojiTypeEmoji,EmojiTypeCustomEmoji). - Added
ChatShareThemeBackdrop,ChatShareThemeBackdrops, andgetChatShareThemeBackdrops.
Gifts
- Added
getUniqueGiftanddeleteBusinessAccountGift.
General
- Added
RichTextTonAddress(withRichTextBuilder.tonAddress()) and theton_address/bank_card_numberentity types (withEntityType.TonAddress/EntityType.BankCardNumber).EntityTypealso gains the previously missingDateTime. - Added
custom_description_parse_modeandcustom_description_entitiestoverifyUserandverifyChat. - Added
PaidMessagesWaivedandMessage.paid_messages_waived.
Rate limiting
rateLimit: { global, perChat }now takes the global and the per-chat token together, once both are available. Before, a request queued behind a busy chat spent its global token while it waited, so a chat backlog could later push sends over the global cap (#1390). Neither token is spent while a request waits on the other, and an aborted wait spends neither.- Added
TokenBucket.takeAll(buckets, signal?), which takes one token from each bucket once all of them have one.
// At most 30 requests/s overall and 1 request/s per chat, both checked at send time
const bot = new Bot(TOKEN, { rateLimit: { global: 30, perChat: 1 } });Full Changelog: v2.3.0...v2.4.0