Recommended resources
The freely available resources this lesson will direct you to.
Hello! In our last lesson, we built our first interactive interfaces using inline keyboards. You learned how to attach buttons with hidden callback data to a message and create specific handlers using bot.callbackQuery() to react to button clicks.
This lesson focuses on a crucial piece of that interaction puzzle: ctx.answerCallbackQuery(). We briefly touched on it as a way to stop the loading animation on a button, but its capabilities go far beyond that. Properly using this method is fundamental to creating the kind of polished, responsive "tech UIs" you aim to build, as it provides direct, immediate feedback to the user. We will explore how to send not just silent acknowledgements, but also customized notifications and alerts.
The Contract of a Callback Query
When a user taps an inline keyboard button, the Telegram client enters a "waiting" state, visually represented by a small loading spinner on the button. The client is waiting for the bot to acknowledge that it has received the callback_query update. This is an explicit contract: your bot must respond to the query within a set time frame (a few seconds).
If your bot fails to respond by calling answerCallbackQuery, the client will eventually time out, the loading spinner will disappear, and from the user's perspective, nothing happened. This is a poor user experience. Therefore, every callbackQuery handler you write must, at some point, call await ctx.answerCallbackQuery().
Think of this as analogous to handling a button click in a web application. You wouldn't want a button to appear "stuck" in a loading state indefinitely after a user clicks it. You'd provide feedback—a toast notification, a spinner, or a change in the UI. answerCallbackQuery is the primary mechanism for that initial feedback in the Telegram ecosystem.
Forms of Acknowledgement
The answerCallbackQuery method is more versatile than it first appears. It can provide three distinct types of feedback.
1. The Silent Acknowledgement
The simplest use case is the one we saw in the previous lesson: a call with no arguments.
bot.callbackQuery("some_data", async (ctx) => {
// Perform some action...
await ctx.answerCallbackQuery(); // Silently acknowledge the click
});
This call simply tells the Telegram client, "I've received the click, you can stop the loading animation." It provides no visible feedback to the user beyond the spinner disappearing. This is appropriate for actions where the primary feedback is a separate, subsequent action, like sending a new message.
2. The Top-of-Screen Notification
For quick, non-intrusive feedback, you can provide a short text string. This text will appear as a transient notification banner at the top of the user's screen and fade away after a few seconds.
You can do this in two ways: by passing the string directly, or using an options object. The grammY documentation shows both.
Inline and Custom Keyboards (built-in) - grammY
This section from the grammY keyboard documentation provides clear examples of how to use answerCallbackQuery with options.
Focus on the code block under the main heading. Notice the line await ctx.answerCallbackQuery({ text: "You were curious, indeed!" });. This demonstrates how to pass an options object to display a notification.
As you saw in the documentation, you provide a text property in an options object:
bot.callbackQuery("do_something", async (ctx) => {
// ... perform logic ...
await ctx.answerCallbackQuery({
text: "Action completed successfully!"
});
});
This is ideal for confirming that a background action has been initiated or completed without interrupting the user's flow by sending a new chat message.
3. The Pop-up Alert
For more critical information—warnings, errors, or confirmations that require the user's explicit attention—you can display a modal alert box. The user must tap "OK" to dismiss it. This is achieved by adding show_alert: true to the options object.

The image above shows exactly what this looks like. The code to produce such an alert is a simple extension of what we've already seen:
bot.callbackQuery("delete_data", async (ctx) => {
// ... logic to delete data ...
await ctx.answerCallbackQuery({
text: "Warning: All data has been permanently deleted.",
show_alert: true,
});
});
This type of feedback is much more intrusive, so it should be used sparingly. It's the equivalent of a web browser's alert() dialog. Use it when you need to be certain the user has seen a message.
Putting It All Together: An Example
Let's consolidate these concepts into a practical example. The following bot provides three buttons, each demonstrating a different type of acknowledgement.
import { Bot, InlineKeyboard } from "grammy";
// Assume bot token is in process.env.BOT_TOKEN
const bot = new Bot(process.env.BOT_TOKEN || "");
const feedbackKeyboard = new InlineKeyboard()
.text("Silent Ack", "silent")
.text("Notification", "notify").row()
.text("Alert!", "alert");
bot.command("start", async (ctx) => {
await ctx.reply("Choose your feedback type:", {
reply_markup: feedbackKeyboard,
});
});
// 1. Silent acknowledgement
bot.callbackQuery("silent", async (ctx) => {
await ctx.answerCallbackQuery();
await ctx.reply("A silent acknowledgement was sent. Did you see the loading spinner disappear?");
});
// 2. Top-of-screen notification
bot.callbackQuery("notify", async (ctx) => {
await ctx.answerCallbackQuery({
text: "Processing...",
});
// You can still perform other actions
await ctx.reply("A notification banner appeared at the top of your screen.");
});
// 3. Pop-up alert
bot.callbackQuery("alert", async (ctx) => {
await ctx.answerCallbackQuery({
text: "This is a modal alert!",
show_alert: true,
});
// Note: The flow continues after the user dismisses the alert.
});
bot.start();
console.log("Bot started!");
I encourage you to run this code. Interacting with the three buttons will give you a concrete feel for how each answerCallbackQuery variant behaves from the user's perspective.
Best Practice: The Catch-All Handler
What happens if a user clicks a button that your bot isn't explicitly listening for? Without a handler, the bot won't call answerCallbackQuery, and the user will be left with an eternally spinning loading icon.
To create a robust and polished experience, it's a best practice to add a "catch-all" handler for any callback queries that haven't been matched by your specific listeners. The bot.on("callback_query:data") listener serves this exact purpose.
Important: This handler should be defined after all your specific bot.callbackQuery("...") handlers. grammY processes middleware in the order it's registered, so this ensures your specific handlers get a chance to run first.
// ... all your specific bot.callbackQuery() handlers go here ...
// The catch-all handler, placed at the end
bot.on("callback_query:data", async (ctx) => {
console.log("Caught an unhandled button click with data:", ctx.callbackQuery.data);
// Silently acknowledge the click to prevent the client from timing out
await ctx.answerCallbackQuery();
});
This small addition makes your bot feel much more professional, ensuring that no user interaction is ever left hanging.
Conclusion
You have now moved beyond simply reacting to button clicks and have learned to control the immediate feedback loop with the user. This is a subtle but vital skill for building advanced bot interfaces that feel responsive and well-engineered.
Key Takeaways:
- Calling
ctx.answerCallbackQuery()is mandatory within anycallbackQueryhandler to fulfill the API contract and provide good UX. - A basic
await ctx.answerCallbackQuery()sends a silent acknowledgement, stopping the button's loading state. - Passing a
textoption, likeawait ctx.answerCallbackQuery({ text: "Done!" }), displays a non-intrusive notification banner at the top of the screen. - Adding
show_alert: trueelevates the notification to a modal pop-up alert that requires user dismissal. - A final
bot.on("callback_query:data", ...)handler should be used as a catch-all to gracefully handle any unexpected button clicks.
In our next lesson, we'll take the final step in building dynamic menus. Now that we can handle clicks and provide feedback, we'll learn how to edit the original message and its keyboard, allowing you to create menus that change their state and content in-place without cluttering the chat history.