Чтобы принять оплату в Telegram Mini App, сервер создает счет, приложение открывает платежное окно, а бот получает подтверждение результата. Для цифрового доступа к курсу внутри Telegram используется Telegram Stars. Для физических товаров предусмотрена отдельная схема со сторонним платежным провайдером. Выбор зависит от того, что именно получает покупатель. Правила Telegram для цифровых товаров.
Продолжить в Telegram
Развиваете бота или Mini App?
Материалы о Telegram, ботах и рекламе публикую в своём канале. Подпишитесь, чтобы следить за новыми разборами.
Какую схему оплаты выбрать
| Что продаете | Схема | Что подготовить |
|---|---|---|
| Цифровой курс, файл, функция приложения или цифровой доступ | Telegram Stars, валюта XTR | Бот, сервер заказов и выдача доступа |
| Физический товар, например печатная книга с доставкой | Платежный провайдер, подключенный к боту | Поддерживаемый провайдер, валюта, доставка и обработка заказа |
В схеме для физических товаров способы оплаты зависят от провайдера и его доступности для вашего бизнеса и покупателя. Telegram описывает поддержку карт, Apple Pay и Google Pay, но это не означает, что любой выбранный провайдер предложит их каждому пользователю. Проверяйте подключение через BotFather и условия самого провайдера. Документация платежей для физических товаров и услуг.
Ниже разобрана разовая покупка цифрового курса за 100 Stars. Это условная цена для учебного примера, без привязки к рублям. Автопродление подписки и доставка физических товаров в этот сценарий не включены.
Последовательность: от кнопки до доступа
- Mini App отправляет серверу данные авторизации и запрос на покупку выбранного продукта.
- Сервер проверяет пользователя, сохраняет заказ и получает ссылку на счет через
createInvoiceLink. - Приложение передает эту ссылку в
Telegram.WebApp.openInvoice(invoiceUrl). - Бот проверяет возможность принять платеж и после подтвержденной оплаты ставит выдачу доступа в очередь.
openInvoice принимает URL готового счета. Объект с ценой и валютой относится к серверному вызову Bot API. Методы Telegram Mini Apps, параметры createInvoiceLink.
Учебный пример серверной части
В примере используется JavaScript для Node.js с fetch. Он показывает связь между заказом и Bot API. Функции authenticateMiniApp, orders и assertWebhookSecret ниже обозначают части вашего проекта, которые необходимо реализовать; это не методы Telegram и не готовый магазин.
// Сервер Node.js. Учебный пример для тестовой среды.
// authenticateMiniApp и orders реализуются в вашем проекте.
const botToken = process.env.TELEGRAM_BOT_TOKEN;
if (!botToken) throw new Error("Не настроен токен тестового бота");
const apiRoot = "https://api.telegram.org/bot" + botToken + "/test/";
async function botApi(method, parameters) {
const response = await fetch(apiRoot + method, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(parameters),
signal: AbortSignal.timeout(5000)
});
const result = await response.json();
if (!response.ok || !result.ok) {
throw new Error("Не удалось выполнить запрос к Bot API");
}
return result.result;
}
// Обработчик POST /api/course/invoice вызывает эту функцию.
async function createCourseInvoice(rawInitData, requestKey) {
const user = await authenticateMiniApp(rawInitData);
const product = { sku: "course_pro", amount: 100, currency: "XTR" };
// Цена берется с сервера. ID заказа — случайный UUID.
// requestKey учитывается вместе с user.id, а не вместо авторизации.
const order = await orders.getOrCreatePending({
userId: String(user.id),
requestKey,
sku: product.sku,
totalAmount: product.amount,
currency: product.currency
});
const invoiceUrl = await botApi("createInvoiceLink", {
title: "Доступ к курсу Pro",
description: "Однократная покупка доступа к учебному курсу",
payload: order.id,
provider_token: "",
currency: order.currency,
prices: [{ label: "Доступ к курсу", amount: order.totalAmount }]
});
return { invoiceUrl, orderId: order.id };
}
Для Stars поле provider_token оставлено пустым, а в prices передается один элемент. В payload записан идентификатор заказа. Бот-токен хранится только на сервере; его нельзя помещать в Mini App, опубликованный код или журнал запросов. Описание полей счета.
orders.getOrCreatePending должен сохранять заказ в постоянной базе и возвращать один и тот же неоплаченный заказ при повторе requestKey от того же пользователя. Это помогает пережить двойное нажатие и сетевой повтор. Цена, валюта и состав покупки берутся из серверного каталога. Клиентский запрос не должен иметь возможности заменить цену или владельца заказа.
Проверка пользователя на сервере
Передавайте исходную строку Telegram.WebApp.initData. Сервер проверяет ее подпись по алгоритму Telegram с токеном нужного бота и учитывает auth_date. Доверять initDataUnsafe как подтверждению личности нельзя. Алгоритм проверки initData.
Контракт authenticateMiniApp в этом примере: отклонить неверную подпись, повторяющиеся или поврежденные поля, отсутствие пользователя, устаревшую авторизацию и дату из будущего; вернуть ID только из проверенных данных. Допустимый срок авторизации задается приложением. Сравнение подписи выполняйте функцией постоянного времени. После входа можно выдавать собственную короткую серверную сессию, чтобы не передавать исходные данные при каждом действии.
Одной проверки входа недостаточно. При чтении заказа или выдаче файла сервер отдельно проверяет, что заказ принадлежит этому пользователю и имеет подходящий статус. Идентификатор заказа в URL не заменяет проверку доступа. Добавьте ограничения частоты запросов и не записывайте полную строку авторизации в логи.
Как открыть счет в Mini App
// В HTML Mini App есть кнопка #buy и текстовый блок #payment-status.
// Официальный Telegram WebApp SDK уже подключен.
const tg = window.Telegram?.WebApp;
const buy = document.querySelector("#buy");
const statusText = document.querySelector("#payment-status");
let requestKey = crypto.randomUUID();
buy.addEventListener("click", async () => {
if (!tg?.initData || typeof tg.openInvoice !== "function") {
statusText.textContent = "Откройте приложение внутри Telegram";
return;
}
buy.disabled = true;
try {
const response = await fetch("/api/course/invoice", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ initData: tg.initData, requestKey })
});
if (!response.ok) throw new Error("Счет не создан");
const { invoiceUrl, orderId } = await response.json();
// openInvoice получает строку URL, а не объект параметров счета.
await new Promise(resolve => tg.openInvoice(invoiceUrl, resolve));
const check = await fetch("/api/orders/status", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ initData: tg.initData, orderId })
});
if (!check.ok) throw new Error("Статус пока недоступен");
const order = await check.json();
statusText.textContent = order.status === "paid"
? "Оплата подтверждена сервером. Проверяем доступ к курсу."
: "Оплата пока не подтверждена. Проверьте заказ немного позже.";
} catch {
statusText.textContent = "Не удалось завершить проверку. Откройте список заказов.";
} finally {
buy.disabled = false;
}
});
Callback платежного окна используется здесь лишь как повод запросить состояние заказа у своего сервера. Он не выдает курс. Обработчик /api/orders/status должен заново авторизовать пользователя и проверить принадлежность заказа. Если уведомление об оплате еще обрабатывается, покажите ожидание и кнопку повторной проверки; не создавайте новый оплаченный заказ на основании клиентского статуса.
Подтверждение платежа и однократная выдача
Telegram присылает pre_checkout_query, на который требуется ответить в течение 10 секунд. Этот этап еще не подтверждает оплату. Для выдачи покупки нужно дождаться successful_payment. Проверка перед оплатой, подтверждение платежа.
// Серверный обработчик. HTTP-адаптер передает тело запроса и заголовок
// X-Telegram-Bot-Api-Secret-Token. assertWebhookSecret и orders — код проекта.
async function handleTelegramUpdate(update, secretHeader) {
assertWebhookSecret(secretHeader);
if (update.pre_checkout_query) {
const q = update.pre_checkout_query;
// Атомарно сверить заказ, покупателя, цену, валюту и срок.
// Повтор того же q.id допустим; второй checkout одного заказа — нет.
const allowed = await orders.reserveCheckout({
queryId: q.id,
orderId: q.invoice_payload,
userId: String(q.from.id),
currency: q.currency,
totalAmount: q.total_amount
});
await botApi("answerPreCheckoutQuery", {
pre_checkout_query_id: q.id,
ok: allowed,
...(allowed ? {} : { error_message: "Заказ недоступен. Создайте новый заказ." })
});
return;
}
const message = update.message;
const payment = message?.successful_payment;
if (!payment) return;
if (!message.from) throw new Error("Не определен покупатель");
// В одной транзакции: проверка заказа, запись платежа, задача выдачи.
// Уникальные ограничения защищают и chargeId, и однократную выдачу orderId.
await orders.recordPaymentAndQueueGrant({
orderId: payment.invoice_payload,
userId: String(message.from.id),
currency: payment.currency,
totalAmount: payment.total_amount,
chargeId: payment.telegram_payment_charge_id
});
}
При регистрации webhook задайте отдельный secret_token. assertWebhookSecret должен сверять полученный заголовок X-Telegram-Bot-Api-Secret-Token с непустым серверным секретом, отклоняя несовпадение до обработки заказа. Используйте HTTPS. Параметры setWebhook.
reserveCheckout выполняет быструю атомарную проверку: заказ существует, принадлежит покупателю, еще допускает оплату, не истек, сумма и валюта совпадают с сохраненными. Не запускайте тяжелую выдачу курса на этом шаге. Повтор того же запроса должен обрабатываться согласованно, а параллельная попытка оплатить тот же заказ — отклоняться. Срок резервирования и повторную попытку после отмены нужно продумать отдельно, чтобы покупатель не оставался с заблокированным заказом.
recordPaymentAndQueueGrant повторно сверяет заказ, покупателя, валюту и сумму, сохраняет платеж и задачу выдачи в одной транзакции. Поставьте уникальные ограничения на идентификатор платежа и выдачу по заказу. Повтор того же уведомления не должен повторно начислять доступ. Если по уже оплаченному заказу пришел другой платеж, сохраните его для разбора и возврата, не выдавая покупку дважды.
Рабочий процесс выдачи берет сохраненную задачу из очереди и также выполняется идемпотентно: повторный запуск приводит к тому же одному доступу. Webhook можно подтверждать успешным HTTP-ответом после надежного сохранения события; временный сбой базы нельзя скрывать как успешно обработанный платеж. Так оплата не потеряется между уведомлением и выдачей.
Больше материалов о Telegram — в моём Telegram-канале.
Как проверить оплату перед запуском
Для Stars используйте отдельную тестовую среду Telegram с тестовыми учетной записью и ботом. В серверном примере запросы специально направлены через /test/. Рабочие и тестовые токены, заказы и адреса webhook храните раздельно. Подключение тестовой среды.
- Обычная успешная покупка: платеж записан, доступ выдан один раз.
- Отмена окна и повторное открытие: неоплаченный заказ не получает доступ.
- Повтор webhook и два одновременных запроса: нет повторного начисления.
- Чужой ID заказа, измененная сумма, неверная валюта или подпись: операция отклонена.
- Сбой после записи платежа, но до выдачи: очередь восстанавливает доступ.
- Задержка уведомления: интерфейс показывает ожидание и позволяет проверить заказ.
Подготовьте понятные условия покупки, контакт поддержки и обработку /paysupport. Для возврата Stars предусмотрен refundStarPayment; сохраняйте идентификатор платежа и связывайте возврат с состоянием доступа. Поддержка покупателей и возвраты.
Платежный сценарий можно считать готовым к рабочим проверкам, когда подтвержденная оплата переживает повтор уведомления и перезапуск сервера, а покупатель получает ровно один доступ и может обратиться за помощью по конкретному заказу.
Продвижение готового бота
После настройки бота и оплаты можно перейти к рекламе. По вопросу открытия еврокабинета Telegram Ads напишите в TeleScope.
Кнопка «Открыть еврокабинет» ведёт в чат поддержки TeleScope.
Или используйте мою ссылку для регистрации в TeleScope.
