Деньги в боте — это подписки, разовые товары и донаты, и Bot API умеет всё три: платёжная схема встроена в Telegram давно, а с 2024 года к фиатным провайдерам добавились внутренние звёзды. Хорошая новость архитектурная: платежи живут в тех же апдейтах, что и сообщения, — новых серверов, портов и источников событий не появляется, бот просто обрабатывает ещё два типа обновлений. Схема одинаковая для любых денег: SendInvoice показывает счёт, пользователь жмёт «Оплатить», бот подтверждает pre-checkout, Telegram списывает и шлёт successful_payment. Меняется только источник списания: токен провайдера для реальных валют или звёзды для цифровых товаров. Сразу договоримся о глубине: это обзорная карта — кабинет провайдера, вебхуки, чеки и налоги останутся за кадром, зато весь путь от инвойса до выдачи товара будет виден целиком и каждый шаг будет с кодом.
Инвойс: tgbotapi.NewInvoice и NewLabeledPrice#
Инвойс — это особое сообщение с кнопкой «Оплатить», которое собирает конфигурация InvoiceConfig, а конструктор tgbotapi.NewInvoice заполняет обязательные поля. Их немного, но каждое работает в платёжной логике: title — название товара до 32 символов, description — строка на экране подтверждения, payload — ваш собственный идентификатор заказа, который Telegram хранит как есть и вернёт после оплаты; его длина — до 128 байт, и в него кладут id заказа из базы. Currency — код валюты, а Prices — срез tgbotapi.LabeledPrice, собранный через NewLabeledPrice(label, amount): сумма в минимальных единицах, для рублей и долларов это копейки и центы, для звёзд — целые штук. Провайдерские инвойсы ещё и разбиваются на компоненты — товар, доставка, скидка, — каждая своим LabeledPrice, и итог считается из этой таблицы. ProviderToken и StartParameter замыкают конфигурацию: токен решает, кто списывает деньги, о нём — следующий раздел.
Отправка инвойса
GOprices := []tgbotapi.LabeledPrice{
tgbotapi.NewLabeledPrice("Подписка «Про», месяц", 29900),
}
invoice := tgbotapi.NewInvoice(
chatID,
"Подписка «Про»", // заголовок, до 32 символов
"Отчёты и экспорт на 30 дней", // описание на экране оплаты
"sub-monthly-1024", // payload: ваш id заказа
os.Getenv("PROVIDER_TOKEN"), // токен провайдера из BotFather
"sub-monthly", // startParameter
"EUR", // валюта
prices,
)
if _, err := bot.Send(invoice); err != nil {
log.Println(err)
}Сумма 29900 при валюте EUR — это 299 евро и 00 центов: LabeledPrice всегда в минимальных единицах, и перепутанная шкала — самая дорогая опечатка в этой теме. Payload sub-monthly-1024 — связка с вашей базой: в successful_payment придёт именно он, по нему выдаётся оплаченный товар. Send уходит обычным методом: инвойс — обычное сообщение с особым содержимым.
Провайдеры и тестовые платежи#
Фиатные деньги в боте идут через платёжного провайдера: Telegram передаёт инвойс подключённой системе, та списывает средства, а бот получает только подтверждение — сами деньги через Telegram не проходят. Токены выдаются в BotFather разделом /payments: поддерживаются Stripe и региональные провайдеры, список с примерами есть в документации Bot API, а выбор обычно диктуется страной и юридическим статусом продавца. Для разработки у большинства провайдеров есть тестовый режим: тестовые токены и тестовые карты дают возможность пройти всю схему — инвойс, pre-checkout, successful_payment — без единой копейки. Проверять удобно в тестовой среде Telegram Desktop, где клиент работает против песочницы. Честно про сложность: реальный фиат тянет за собой кабинет провайдера, вебхуки о статусах, возвраты и договор — это отдельный проект, и обзорный урок сознательно не притворяется, что покрывает его двадцатью строками. Карта пути та же самая, меняется только обвязка вокруг неё.
Telegram Stars: XTR и пустой provider_token#
Звёзды — внутренняя валюта Telegram, появившаяся в 2024 году, и для цифровых товаров в ботах она стала обязательной дорогой: правила магазинов приложений требуют, чтобы цифровой контент продавался через встроенную оплату, и звёзды — это та самая встроенная оплата. Схема для разработчика почти не меняется: в инвойсе ставится Currency XTR, ProviderToken пустая строка, а суммы в Prices считаются целыми звёздами — никакого деления на сотни. Подтверждение и successful_payment те же, возврат делается методом Bot API refundStarPayment, который свежие обёртки библиотеки дают соответствующей конфигурацией. Звёзды же обслуживают периодические подписки ботов и платные медиа — в этом уроке только отметим их существование, а инвойс в звёздах собирается так же, как обычный: те же поля, другая валюта. Для пет-проекта это идеальный вход в платежи: ни договора с провайдером, ни тестовых карт — минимальный инвойс оплачивается парой звёзд.
Pre-checkout и successful_payment
GOfor update := range updates {
switch {
case update.PreCheckoutQuery != nil:
// между нажатием «Оплатить» и списанием: 10 секунд на решение
ok := tgbotapi.PreCheckoutQueryConfig{
PreCheckoutQueryID: update.PreCheckoutQuery.ID,
OK: true,
}
_, _ = bot.Request(ok)
case update.Message != nil && update.Message.SuccessfulPayment != nil:
p := update.Message.SuccessfulPayment
log.Printf("оплачен заказ %s: %d %s, charge %s",
p.InvoicePayload, p.TotalAmount, p.Currency,
p.TelegramPaymentChargeId)
grantPurchase(p.InvoicePayload, p.TelegramPaymentChargeId)
}
}PreCheckoutQuery приходит после нажатия кнопки и до списания: у бота десять секунд ответить OK или отклонить с сообщением об ошибке — молчание отмена трактует как отказ. В ветке успешной оплаты значимые поля: InvoicePayload, по которому выдаётся товар, и TelegramPaymentChargeId — уникальный идентификатор транзакции, он же ключ возврата и ключ идемпотентности выдачи.
Хендлер pre-checkout пошёл в базу проверять наличие товара, база тормозит на фоне деплоя, десять секунд истекли — Telegram отменил платёж, пользователь увидел ошибку, и единственная информация о попытке — в логах. Проверка в этом хендлере обязана быть лёгкой: посмотреть в кэш или память, при сомнении — отказать с внятным сообщением и предложить попробовать снова, это нормальный исход. Вторая ловушка ждёт после оплаты: webhook-доставка повторяет апдейт при недоступности сервера, и без записи charge_id выданного товара повторный successful_payment выдаст подписку второй раз. Идемпотентность — запоминание TelegramPaymentChargeId и пропуск обработанного — ставится в тот же вечер, когда появляется первый платёж, а не после первого двойного.
Что приходит после кнопки «Оплатить»#
Весь диалог денег укладывается в два апдейта и один ответ. PreCheckoutQuery несёт ID запроса, валюту, сумму и ваш payload — достаточно данных, чтобы отказать по делу: товар сняли с продажи, лимит исчерпан, пользователь из заблокированной страны. Ответ конфигурацией PreCheckoutQueryConfig с OK true открывает списание; OK false с ErrorMessage до пары десятков слов объясняет причину — этот текст пользователь увидит в диалоге оплаты. Дальше успешная ветка: SuccessfulPayment внутри Message приносит InvoicePayload, TotalAmount, Currency и пару идентификаторов — TelegramPaymentChargeId и ProviderPaymentChargeId. По payload выдаётся товар, charge_id записывается как факт выдачи и страховка от повторов. Для звёзд на этом всё — деньги уже внутри Telegram; для провайдера начинается его половина работы: чеки, статусы и возвраты в его кабинете. Статусы заказов хранятся у себя — хэш в Redis на каждый payload отлично закрывает эту роль.
Обзорная карта платежа#
| Этап | Кто инициирует | Ваш код | Результат |
|---|---|---|---|
| Инвойс | Бот | NewInvoice + bot.Send | Сообщение с кнопкой «Оплатить» |
| Pre-checkout | Telegram | PreCheckoutQueryConfig, OK true/false | Разрешение или вежливый отказ до списания |
| Оплата | Пользователь | ничего — Telegram списывает | successful_payment в Message |
| Выдача | Бот | grantPurchase по payload | Товар у пользователя, charge_id записан |
| Возврат | Бот | refundStarPayment или кабинет провайдера | Деньги вернулись, статус обновлён |
Что осталось за кадром — и об этом честно. Реальный фиат: договор с провайдером, KYC, вебхуки о промежуточных статусах, чеки и налоги по юрисдикции — у каждого провайдера своя документация, и объём этой части выше всего курса про ботов. Подписки через звёзды: отдельные методы на выдачу, продление и отмену, плюс служебные апдейты об их изменении. Mini App со своей корзиной: интерфейс покупки переезжает в веб-вью, но бэкенд остаётся ровно этой схемой. Проверка платежа на своей стороне: суммы и валюты стоит сверять с ожидаемыми, payload валидировать — successful_payment верить, но перепроверять. Обзорная карта не делает вас платёжным разработчиком, зато снимает страх перед темой: четыре этапа, два апдейта, один ответ — остальное обвязка вокруг уже знакомого цикла range.
Понадобятся токен бота из BotFather и локальный Go. Для фиата возьмите тестовый токен провайдера из раздела /payments и включите тестовую среду в Telegram Desktop — пройдёте инвойс и оплату тестовой картой. Для звёзд дешевле всего отправить инвойс самому себе: Currency XTR, пустой provider_token, цена в пару звёзд — и весь путь от pre-checkout до successful_payment пройдёт по-настоящему. Логи handler-а покажут оба апдейта целиком — сохраните их как фикстуры для тестов из урока про тестирование бота.
Проверка: инвойс в звёздах
Что нужно поменять в инвойсе, чтобы принимать Telegram Stars?
Проверка: окно pre-checkout
Сколько времени у бота на ответ на pre_checkout_query?
Медленная проверка перед списанием
Прочитайте код и выберите, что он выведет:
case update.PreCheckoutQuery != nil:
// хендлер проверяет наличие товара в базе
// и на фоне деплоя база отвечает 15 секунд
ok := tgbotapi.PreCheckoutQueryConfig{
PreCheckoutQueryID: update.PreCheckoutQuery.ID,
OK: true,
}
_, _ = bot.Request(ok)Соберите платёжные ветки
Впишите поля апдейтов и подтверждение pre-checkout.
Соберите путь платежа
Расставьте строки: от инвойса до выдачи товара.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: платежи
Карта собрана — дальше маршрут курса: следующий урок про Mini Apps — веб-интерфейс внутри Telegram, куда переезжает витрина для сложных товаров; урок про ошибки и лимиты — почему массовые инвойсы подчиняются тем же 429, что и рассылки; урок про тесты бота — как покрыть платёжные ветки без Telegram, сохранёнными апдейтами. Откуда платёжная механика растёт: цикл апдейтов и webhook — восемнадцатый урок, деплой на VPS — обязательный шаг, прежде чем принимать деньги: платежи не прощают перезапусков без логов. Хранение статусов заказов в Redis разобрано в уроке про хэши курса go-redis. Полный маршрут — на странице курса.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Платёж — четыре этапа и два апдейта: NewInvoice и Send, pre_checkout_query с ответом за десять секунд, successful_payment, выдача по payload.
- LabeledPrice считает в минимальных единицах: копейки и центы для валют, целые звёзды для XTR — перепутанная шкала дороже всего остального.
- Фиат идёт через провайдера: токен из BotFather /payments, тестовый режим для разработки, обвязка провайдера — отдельный проект.
- Telegram Stars: Currency XTR и пустой provider_token; цифровые товары в ботах продаются через звёзды, возврат — refundStarPayment.
- TelegramPaymentChargeId — ключ идемпотентности выдачи и возврата; без него повтор доставки апдейта выдаёт товар дважды.
- Урок обзорный: вебхуки провайдера, чеки, налоги и подписки через звёзды остались картой направлений, а не инструкцией.
Домашнее задание
Соберите платёжный стенд: бот с командой /buy, которая отправляет инвойс в звёздах — Currency XTR, пустой provider_token, цена в одну-две звезды, payload вида test-order-1. Обработайте pre_checkout_query с лёгкой проверкой payload и successful_payment с записью charge_id в Redis-хэш. Оплатите инвойс сами себе и убедитесь, что повторная доставка successful_payment не выдаёт товар второй раз — добавьте идемпотентность по TelegramPaymentChargeId. Затем отправьте себе отказ: OK false с сообщением о снятом с продажи товаре.
Личные заметкиПусто
Вопросы и ответы по уроку
Можно ли принимать рубли и доллары без провайдера?
Нет: фиатные валюты идут только через подключённого провайдера — Telegram не держит ваши деньги и не выдаёт их боту. Обходные схемы в виде «переводов на карту» вне платежной схемы возможны технически, но остаются ручным учётом без подтверждений и возвратов — это не платежи бота, а договорённости вне системы.
Чем Stars лучше провайдера для пет-проекта?
Меньше обвязки: нет договора, кабинета и вебхуков провайдера, инвойс отличается только валютой XTR и пустым токеном, а вся схема — pre-checkout и successful_payment — та же. Минус тоже честный: звёзды — внутренняя валюта, и вывод заработанного идёт через конвертацию, а пользователи должны сначала звёзды купить.
Что делать, если pre-checkout ответил OK, а бот упал до выдачи товара?
Платёж состоится, successful_payment придёт — возможно, с повторами доставки при webhook. Отсюда идемпотентность: выдача товара привязана к payload, а факт выдачи записывается с charge_id; обработчик, увидев знакомый charge_id, просто подтверждает обработку без повторной выдачи. Деньги не теряются, если выдача вынесена из опасной зоны и основана на апдейте, а не на памяти процесса.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Mini Apps: веб-интерфейс внутри Telegram
- Реакции, стикеры и живой тон бота
- Тестирование бота: хендлеры без Telegram
- Деплой на VPS: systemd и логи
- Напоминания: goroutine и тикер
- Webhook: бот как HTTP-сервер
- Ошибки и лимиты Telegram: 429 и RetryAfter
- Редактирование сообщений и многостраничное меню
- Все курсы по Go
Следующая ступень учебного пути
Мониторинг Go с Prometheus: метрики, Grafana, алерты
Курс по метрикам в Go: Counter, Gauge, Histogram, лейблы, PromQL, Grafana-дашборды, алерты и бизнес-метрики. 25 уроков с примерами client_golang.
- Понимать четыре типа метрик и выбирать правильный
- Экспортировать метрики из Go через promhttp
- Инструментировать HTTP-запросы и бизнес-события
- Писать PromQL-запросы: rate, sum, p99