Принимаем платежи прямо в боте
Раньше чтобы продать что-то через бота, надо было редиректить пользователя на сайт с формой оплаты. Это убивало конверсию. Теперь Telegram Payments: пользователь нажимает кнопку «Оплатить», вводит карту прямо в Telegram (один раз — потом сохраняется), и через 5 секунд деньги у тебя на счету. Конверсия как в App Store. Разберём как это работает технически и какие есть ловушки.
Как устроены платежи в Telegram
Telegram сам платежи НЕ обрабатывает. Он берёт на себя UX (форма ввода карты, 3D Secure, чек), а реальные деньги идут через провайдеров: Stripe, ЮKassa, Tinkoff, Sberbank, Paymaster. У каждого провайдера свой токен, который ты получаешь после регистрации у них и привязываешь к боту через @BotFather. Поток: бот шлёт invoice → юзер нажимает «Заплатить» → Telegram показывает форму → юзер вводит карту → Telegram шлёт боту pre_checkout_query (последний шанс отказать) → если ОК → Telegram проводит платёж через провайдера → присылает боту successful_payment.
Регистрация провайдера
Идёшь к @BotFather, /mybots → твой бот → Payments. Там список провайдеров. Выбираешь например Stripe (для глобального) или ЮKassa (для России). Тебя редиректит на сайт провайдера, регистрируешь юрлицо/самозанятого, получаешь Provider Token — длинная строка вида 1234567:TEST:abcdef. TEST — это тестовый режим, для разработки. Для прода нужен LIVE-токен (после прохождения KYC). Этот токен бот использует при отправке invoice.
Цифровые товары и Telegram Stars
Для цифровых товаров (подписки, разблокировка фич, токены в игре) Apple/Google требуют использовать ИХ платежи на iOS/Android. Чтобы не нарушать правила, Telegram сделал Telegram Stars — внутренняя валюта где Apple/Google уже получили свою долю. Для цифровых товаров используй ТОЛЬКО Stars. Для физических товаров (доставка, услуги) — обычные провайдеры (Stripe и др). Если торгуешь цифрой через Stripe в боте — Apple может забанить пользователя.
pre_checkout_query — последний шанс отказать
Перед фактическим списанием Telegram шлёт боту pre_checkout_query. У тебя 10 секунд чтобы ответить ok=true или ok=false с причиной. Здесь делаешь финальные проверки: товар ещё в наличии? Юзер не в бане? Цена не изменилась? Если ok=false → платёж отменяется, юзеру показывается твоя причина. Если не ответить за 10 секунд — Telegram отменит транзакцию автоматически. Это критично! Никогда не оставляй pre_checkout без обработчика.
Идемпотентность и подписки
Самое важное в платежах — не давать товар дважды. Каждый успешный платёж имеет уникальный telegram_payment_charge_id — записывай его в БД с уникальным constraint. На successful_payment ищешь этот id — если уже есть, ничего не делаешь (уже выдали). Для подписок: Telegram Stars поддерживают recurring (подписку с автосписанием), у внешних провайдеров надо самому управлять — после оплаты ставишь флаг «активна до 2026-05-01», cron каждый день проверяет истечения и просит оплатить снова.
import { Bot, InlineKeyboard } from 'grammy';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const bot = new Bot(process.env.BOT_TOKEN!);
const PROVIDER_TOKEN = process.env.PROVIDER_TOKEN!; // от @BotFather
// 1. Юзер запросил оплату
bot.command('buy_pro', async (ctx) => {
await ctx.replyWithInvoice(
'Pro подписка', // title
'Безлимит на месяц', // description
`pro_month:${ctx.from!.id}`, // payload — твой ID транзакции
'XTR', // currency: XTR = Telegram Stars,
// или 'USD', 'RUB' для обычных провайдеров
[{ label: 'Pro 1 месяц', amount: 100 }] // amount в минимальных единицах:
// для USD это центы, для Stars — штуки
// если провайдер обычный (не Stars), второй аргумент — provider_token:
// { provider_token: PROVIDER_TOKEN }
);
});
// 2. Pre-checkout — последний шанс отказать
bot.on('pre_checkout_query', async (ctx) => {
// Здесь финальные проверки
const userId = ctx.from!.id;
const user = await prisma.user.findUnique({ where: { id: BigInt(userId) } });
if (user?.banned) {
return ctx.answerPreCheckoutQuery(false, 'Аккаунт заблокирован');
}
// Всё ок — подтверждаем
return ctx.answerPreCheckoutQuery(true);
});
// 3. Успешный платёж — выдаём товар, идемпотентно
bot.on(':successful_payment', async (ctx) => {
const payment = ctx.message?.successful_payment;
if (!payment) return;
const chargeId = payment.telegram_payment_charge_id;
// Идемпотентность — проверяем не выдавали ли уже
const existing = await prisma.payment.findUnique({
where: { chargeId },
});
if (existing) return; // уже обработали
// Записываем платёж
await prisma.payment.create({
data: {
chargeId,
userId: BigInt(ctx.from!.id),
amount: payment.total_amount,
currency: payment.currency,
payload: payment.invoice_payload,
},
});
// Активируем Pro на месяц
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
await prisma.user.update({
where: { id: BigInt(ctx.from!.id) },
data: { plan: 'pro', planExpiresAt: expiresAt },
});
await ctx.reply('🎉 Pro активирован на 30 дней!');
});
bot.start();Продажа доступа к Pro-плану через Telegram Payments