We are building a Node.js Telegram bot flow that sends a message with inline Approve and Reject buttons. When you click a button, the bot processes the decision, updates the original message to reflect the choice, and removes the buttons.
This is useful for admin tasks like moderating user content, authorizing payments, or approving database writes.
Sending the message with inline buttons
To interact with the Telegram API, we will use the node-telegram-bot-api package. You will need a bot token from BotFather to authenticate your requests.
The bot sends a message containing an inline keyboard. Each button needs a callback_data string. This is the payload Telegram sends back to your server when the user clicks the button.
import TelegramBot from 'node-telegram-bot-api';
const token = process.env.TELEGRAM_BOT_TOKEN as string;
const bot = new TelegramBot(token, { polling: true });
async function sendApprovalRequest(chatId: number, userId: string, content: string) {
const text = `New user signup pending approval:\n\nUser ID: ${userId}\nContent: ${content}`;
await bot.sendMessage(chatId, text, {
reply_markup: {
inline_keyboard: [
[
{ text: 'Approve', callback_data: `approve_${userId}` },
{ text: 'Reject', callback_data: `reject_${userId}` }
]
]
}
});
}
Listening for the callback query
When a user taps an inline button, Telegram sends a callback_query event to your bot. Listen for this event and extract the data string.
The callback_query object contains the data string you defined earlier, the message object, and a unique query ID. We split the string to separate the action type from the user ID.
bot.on('callback_query', async (query) => {
const chatId = query.message?.chat.id;
const messageId = query.message?.message_id;
const data = query.data;
if (!chatId || !messageId || !data) return;
const [action, userId] = data.split('_');
if (action === 'approve') {
await approveUser(userId);
} else if (action === 'reject') {
await rejectUser(userId);
}
});
async function approveUser(userId: string) {
console.log(`Approved ${userId}`);
}
async function rejectUser(userId: string) {
console.log(`Rejected ${userId}`);
}
Updating the message and clearing state
After you process the action, tell Telegram you received the callback by calling the answerCallbackQuery method with the query ID. If you skip this step, the button will show a loading spinner on the user's device for several seconds.
To remove the buttons, use the editMessageText method to replace the original text with a final status. Passing an empty array to inline_keyboard clears the buttons from the chat.
bot.on('callback_query', async (query) => {
const chatId = query.message?.chat.id;
const messageId = query.message?.message_id;
const data = query.data;
if (!chatId || !messageId || !data) return;
const [action, userId] = data.split('_');
let newText = query.message?.text || '';
if (action === 'approve') {
await approveUser(userId);
newText += '\n\nStatus: Approved';
} else if (action === 'reject') {
await rejectUser(userId);
newText += '\n\nStatus: Rejected';
}
await bot.editMessageText(newText, {
chat_id: chatId,
message_id: messageId,
reply_markup: { inline_keyboard: [] }
});
await bot.answerCallbackQuery(query.id);
});
Things that bit me
The callback_data string has a hard limit of 64 bytes. If you try to stuff a long JSON object or a full URL into it, the Telegram API will reject the message and your bot will crash. Store complex state in your database and only pass short IDs in the callback data.
Multiple people in a group chat can click the buttons at the exact same time. If the message is in a shared group, your bot might receive and process duplicate callback requests before the first one finishes editing the message. You should handle idempotency in your database logic so a record is not approved twice.
FAQ
How do I stop the button from spinning in Telegram?
Call answerCallbackQuery with the query ID. This tells the Telegram app that your server received the click and stops the loading spinner.
How do I remove the inline buttons after a click?
Use the editMessageText method on the original message. Pass an empty inline_keyboard array in the reply_markup property to clear the buttons.
Why does my Telegram bot fail to send the inline keyboard?
Your callback_data string might be too long. Telegram limits the callback data to 64 bytes. Pass short identifiers instead of large payloads.
Originally published at watershowdigital.com/blog/telegram-approve-reject-bot-nodejs. Need a hand with your app or automation? WaterShow Digital fixes and builds it, fixed quote first.
Top comments (0)