tabpayДокументация
Разделы документации

Документация/Рецепты

Оплата в Telegram-боте

Готовый флоу для бота: платёж, кнопка, вебхук, выдача товара

Типовой сценарий интеграции TabPay - продажи в Telegram-боте: цифровые товары, подписки, доступы. Поле telegramId избавляет от необходимости вести собственную таблицу соответствий «заказ - пользователь»: идентификатор покупателя возвращается в уведомлении об оплате.

Понадобится API-ключ активного магазина (раздел Аутентификация) и сохранённый Webhook URL в настройках магазина.

Шаг 1. Кнопка «Купить» создаёт платёж

Бот создаёт платёж и отправляет кнопку со ссылкой на payUrl; на платёжной странице покупатель выбирает СБП или карту.

import aiohttp
from aiogram import Bot, Dispatcher, F
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup

API_KEY = "tp_ваш_ключ"  # API-ключ магазина из кабинета
bot = Bot("ТОКЕН_БОТА")
dp = Dispatcher()


@dp.callback_query(F.data == "buy")
async def buy(callback: CallbackQuery):
    # Создаём платёж и передаём telegramId покупателя -
    # он вернётся в вебхуке, и бот сразу поймёт, кому выдавать товар
    async with aiohttp.ClientSession() as session:
        response = await session.post(
            "https://tabpay.org/api/v1/payments",
            headers={"X-Api-Key": API_KEY},
            json={
                "amountKopecks": 19900,
                "orderId": f"tg-{callback.id}",
                "description": "Подписка на месяц",
                "telegramId": callback.from_user.id,
            },
        )
        payment = await response.json()

    keyboard = InlineKeyboardMarkup(inline_keyboard=[[
        InlineKeyboardButton(text="Оплатить 199,00 ₽", url=payment["payUrl"])
    ]])
    await callback.message.answer("Счёт готов - оплатите по кнопке:", reply_markup=keyboard)
    await callback.answer()

Полное описание полей - в разделе Создание платежа. orderId должен быть уникальным в рамках магазина - в примерах используется идентификатор колбэка. Если бот продаёт несколько товаров, передайте контекст в metadata (например, id товара и тариф) - объект вернётся в вебхуке без изменений, и бот сразу знает, что выдавать.

Шаг 2. Вебхук выдаёт товар

После оплаты TabPay отправляет POST-запрос на Webhook URL магазина; тело уведомления содержит telegramId покупателя:

Тело вебхука
{
  "id": "6b9d2c88-4f1a-4c0e-9b7d-2f8e5a1c3d90",
  "orderId": "tg-4382920412471814230",
  "status": "SUCCESS",
  "amountKopecks": 19900,
  "telegramId": "987654321",
  "metadata": null,
  "test": false
}

Обработчик проверяет подпись X-Signature-V2 (HMAC-SHA256 от строки «метка.тело» по сырым байтам секретом магазина; свежесть X-Timestamp - окно 5 минут; подробнее - в разделе Проверка подписи) и выдаёт товар:

import hashlib
import hmac
import json
import time

from aiohttp import web

WEBHOOK_SECRET = "секрет_подписи_из_кабинета"


async def tabpay_webhook(request: web.Request):
    # Подпись v2 считается от строки "метка.тело" по СЫРЫМ байтам - до разбора JSON
    raw = await request.read()
    timestamp = request.headers.get("X-Timestamp", "")
    received = request.headers.get("X-Signature-V2", "")
    signed = timestamp.encode() + b"." + raw
    expected = hmac.new(WEBHOOK_SECRET.encode(), signed, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, received):
        return web.Response(status=400)

    # Устаревшая метка - повтор перехваченного уведомления
    if abs(time.time() - int(timestamp)) > 300:
        return web.Response(status=401)

    event = json.loads(raw)

    # Тестовое уведомление из кабинета: подтверждаем, но товар не выдаём
    if event.get("test"):
        return web.Response(status=200)

    if event["status"] == "SUCCESS" and event["telegramId"]:
        # Идемпотентность: пометьте event["id"] обработанным,
        # повторный вебхук с тем же id - просто отвечайте 200
        await bot.send_message(
            int(event["telegramId"]),
            "Оплата получена - выдаём товар.",
        )
    return web.Response(status=200)


app = web.Application()
app.router.add_post("/tabpay/webhook", tabpay_webhook)
web.run_app(app, port=8080)

Проверка интеграции без платежа

В настройках магазина на вкладке Webhook доступна кнопка «Отправить тестовый вебхук»: она отправляет на ваш URL уведомление, собранное как боевое, с корректной подписью, но с test: true (в боевых уведомлениях поле равно false). Обработчик должен ответить 200 и не выдавать товар - как в примерах выше. Так проверяются подпись и доставка уведомлений без реального платежа.

На что обратить внимание

  • Выдавайте товар только при status: "SUCCESS" - другие статусы (справочник) означают, что деньги не получены.
  • Вебхук может прийти повторно (без ответа 2xx доставка повторяется, всего до 7 попыток с нарастающим интервалом) - помечайте платёж по id обработанным и не выдавайте товар дважды.
  • telegramId возвращается строкой - при необходимости приведите его к числу перед отправкой сообщения.
  • Если уведомление не было получено (например, сервер бота был недоступен), статус платежа можно сверить запросом Получение платежа.
  • Чтобы после оплаты покупатель вернулся в чат с ботом, передайте при создании платежа successUrl со ссылкой вида https://t.me/ваш_бот - платёжная страница переправит покупателя туда после успешной оплаты.

Если интеграцию выполняет ИИ-ассистент, передайте ему материалы со страницы ИИ и инструменты - готовый промпт и llms.txt включают этот рецепт.