How to Build a Lead Generation Telegram Bot with Node.js and grammY: Step-by-Step Conversations, Webhooks, and 429 Protection

Many developers starting new Node.js projects still choose the node-telegram-bot-api library, despite its architecture being outdated and its support for modern Telegram Bot API features leaving much to be desired. Today, the industry standard for JavaScript and TypeScript is the grammY framework. It is designed around middleware chains (similar to Express and Koa), supports plugins for sessions and step-by-step scenarios, and provides built-in protection against exceeding API rate limits.

In this guide, we will build a production-ready bot for accepting client applications. We will move away from simple "command-response" primitives and implement a full-fledged step-by-step dialogue with data saved to a database, set up a secure Webhook, and bypass the 429 Too Many Requests limit.

Why grammY over node-telegram-bot-api or Telegraf

The main advantage of grammY is its incoming updates processing architecture. Each Update passes through a chain of middleware functions. This allows for flexible isolation of authentication, sessions, and command handling. In addition, grammY solves the problem of dropping stuck requests out of the box and offers official modules for working with conversations (@grammyjs/conversations) and automatic retries on erroneous HTTP responses (@grammyjs/auto-retry).

Safe Start and Code Organization

Never store your bot token in the body of a file. The token gives full control over your bot and allows sending messages on behalf of your company. Use переменные окружения (process.env.BOT_TOKEN) and verify their presence when starting the application.

To build the lead generation scenario, we will need the following packages:

  • grammy — the core of the framework;
  • @grammyjs/conversations — a plugin for implementing step-by-step dialogues (State Machine);
  • express — an HTTP server to receive Webhooks.

Implementing Step-by-Step Lead Collection (Conversations)

Instead of manually storing the user's current step (for example, step = 'awaiting_phone') in a global object or database, the conversations module allows you to write asynchronous linear code. The function "goes to sleep" while waiting for a response from the user and wakes up upon receiving the next message.

Below is a working example of initializing the bot and creating a lead registration flow. Upon successful completion of the dialogue, we generate a unique application ID using the crypto module and send the data to external storage.

const { Bot, session } = require('grammy');
const { conversations, createConversation } = require('@grammyjs/conversations');
const crypto = require('crypto');

const botToken = process.env.BOT_TOKEN;
if (!botToken) {
throw new Error('Критическая ошибка: переменная окружения BOT_TOKEN не задана!');
}

const bot = new Bot(botToken);

// Инициализация хранилища сессий для плагина conversations
bot.use(session({ initial: () => ({}) }));
bot.use(conversations());

/**
* Пошаговый сценарий сбора данных лида
*/
async function collectLeadConversation(conversation, ctx) {
await ctx.reply('Здравствуйте! Как к вам обращаться? Напишите ваше имя:');

const nameCtx = await conversation.waitFor('message:text');
const userName = nameCtx.message.text.trim();

await ctx.reply(`Приятно познакомиться, ${userName}! Укажите ваш контактный номер телефона:`);

const phoneCtx = await conversation.waitFor('message:text');
const userPhone = phoneCtx.message.text.trim();

// Генерация уникального идентификатора заявки
const leadId = crypto.randomBytes(7).toString('hex');
const telegramId = ctx.from.id;

// Безопасный вызов внешних асинхронных функций внутри conversation
await conversation.external(async () => {
// В реальном проекте здесь вызывается INSERT в PostgreSQL / MySQL / MongoDB
console.log(`[БД] Сохранение лида #${leadId}:`, {
id: leadId,
telegramId,
name: userName,
phone: userPhone,
createdAt: new Date().toISOString()
});
});

await ctx.reply(`Спасибо! Ваша заявка #${leadId} принята. Менеджер свяжется с вами в ближайшее время.`);
}

// Регистрация диалога в боте
bot.use(createConversation(collectLeadConversation));

// Команда запуска бота
bot.command('start', async (ctx) => {
await ctx.reply('Вас приветствует отдел продаж! Для подачи заявки нажмите /lead');
});

// Запуск сценария сбора лида
bot.command('lead', async (ctx) => {
await ctx.conversation.enter('collectLeadConversation');
});

Protection Against the 429 Too Many Requests Error

The Telegram Bot API imposes strict limits on the rate of sending messages:

  • No more than 30 messages per second across all users;
  • No more than 1 message per second to the same private chat;
  • No more than 20 messages per minute to a single group or channel.

If you perform a mass broadcast or experience a spike in traffic, Telegram will return an error with HTTP status code 429 and a Retry-After header. Simply attempting to ignore this error will cause user messages to be lost.

In grammY, this issue is solved by attaching the @grammyjs/auto-retry API transformer. This plugin intercepts 429 errors, temporarily pauses sending to the specific chat, and retries the request after the time interval specified by the Telegram server.

const { autoRetry } = require('@grammyjs/auto-retry');

// Подключение авто-ретрая к API-клиенту бота
bot.api.config.use(
autoRetry({
maxRetryAttempts: 3, // Максимальное количество повторных попыток
maxDelaySeconds: 60, // Максимальное время ожидания (в секундах)
})
);

// Глобальный обработчик ошибок (Catch-all)
bot.catch((err) => {
const ctx = err.ctx;
console.error(`Ошибка при обработке update_id ${ctx.update.update_id}:`);
const e = err.error;
if (e instanceof Error) {
console.error('Сообщение об ошибке:', e.message);
}
});

Setting Up Webhooks for Production vs Long Polling for Development

During local development, it is more convenient to use the bot.start() method (Long Polling). However, in a production environment under high load, a Webhook is required.

When using Webhooks, ensuring security is critical: Telegram delivers updates to you over a public URL. To prevent attackers from forging Updates by sending POST requests to your server, use the secret_token parameter. Telegram attaches this token to every HTTP request in the X-Telegram-Bot-Api-Secret-Token header.

const express = require('express');
const { webhookCallback } = require('grammy');

const app = express();
app.use(express.json());

const SECRET_TOKEN = process.env.WEBHOOK_SECRET || 'a_very_secret_hex_string_64_chars';
const PORT = process.env.PORT || 3000;
const WEBHOOK_URL = process.env.WEBHOOK_URL; // Например: https://api.yourdomain.com/telegram/webhook

// Middleware для проверки секретного токена от Telegram
app.use('/telegram/webhook', (req, res, next) => {
const incomingToken = req.headers['x-telegram-bot-api-secret-token'];
if (incomingToken !== SECRET_TOKEN) {
console.warn('[Security Warning] Отклонен запрос с невалидным secret_token');
return res.status(403).json({ error: 'Access denied: invalid secret token' });
}
next();
});

// Передача обработки входящих данных в grammY
app.post('/telegram/webhook', webhookCallback(bot, 'express'));

if (process.env.NODE_ENV === 'production') {
app.listen(PORT, async () => {
console.log(`Webhook сервер успешно запущен на порту ${PORT}`);

// Установка вебхука в Telegram API при старте сервера
await bot.api.setWebhook(WEBHOOK_URL, {
secret_token: SECRET_TOKEN,
drop_pending_updates: true // Пропускаем накопившиеся сообщения при перезапуске
});
console.log(`Webhook установлена на URL: ${WEBHOOK_URL}`);
});
} else {
// Режим разработки: запускаем Long Polling
bot.start({
onStart: (botInfo) => {
console.log(`Бот @${botInfo.username} запущен локально в режиме Long Polling`);
},
});
}

Bot Production Readiness Checklist

  1. All secrets in ENV: No API tokens or DB keys in plain source code.
  2. Idempotency and duplicates: A Webhook may resend the same update_id in case of network glitches. Save processed update_ids in Redis or Postgres and return a 200 OK status within 1–2 seconds.
  3. Логирование ошибок: Be sure to configure bot.catch() so that the failure of a single session does not crash the entire Node.js process.
  4. Rate limiting control: Always enable @grammyjs/auto-retry to protect against 429 error bans.

If you need qualified assistance in designing Telegram-бот architecture, creating Mini Apps, or integrating CRM systems, contact the specialists at BotCreator.

New articles on Telegram

We explain what to automate in your business and how it works in practice. No spam.