Documentation sections
Documentation/Recipes
Payments in a Telegram bot
Ready-made bot flow: payment, button, webhook, delivery
A typical TabPay integration scenario is selling in a Telegram bot: digital goods, subscriptions, access. The telegramId field removes the need to keep your own order-to-user mapping table: the buyer's identifier is returned in the payment notification.
You will need the API key of an active shop (see the Authentication section) and a Webhook URL saved in the shop settings.
Step 1. The Buy button creates a payment
The bot creates a payment and sends a button with a link to payUrl; on the payment page the buyer chooses SBP or a card.
import aiohttp
from aiogram import Bot, Dispatcher, F
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
API_KEY = "tp_your_key" # Shop API key from the dashboard
bot = Bot("BOT_TOKEN")
dp = Dispatcher()
@dp.callback_query(F.data == "buy")
async def buy(callback: CallbackQuery):
# Create a payment and pass the buyer's telegramId -
# it comes back in the webhook, so the bot knows right away who gets the product
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": "Monthly subscription",
"telegramId": callback.from_user.id,
},
)
payment = await response.json()
keyboard = InlineKeyboardMarkup(inline_keyboard=[[
InlineKeyboardButton(text="Pay 199.00 ₽", url=payment["payUrl"])
]])
await callback.message.answer("Your invoice is ready - pay via the button:", reply_markup=keyboard)
await callback.answer()The full field reference is in Create a payment. orderId must be unique within the shop - the examples use the callback identifier. If the bot sells more than one product, pass the context in metadata (for example, the product id and tariff) - the object is echoed back in the webhook, so the bot knows exactly what to deliver.
Step 2. The webhook delivers the product
After the payment TabPay sends a POST request to the shop's Webhook URL; the notification body contains the buyer's telegramId:
{
"id": "6b9d2c88-4f1a-4c0e-9b7d-2f8e5a1c3d90",
"orderId": "tg-4382920412471814230",
"status": "SUCCESS",
"amountKopecks": 19900,
"telegramId": "987654321",
"metadata": null,
"test": false
}The handler verifies the X-Signature-V2 signature (HMAC-SHA256 of the "timestamp.body" string over the raw bytes with the shop secret; X-Timestamp freshness within a 5-minute window; see Signature verification for details) and delivers the product:
import hashlib
import hmac
import json
import time
from aiohttp import web
WEBHOOK_SECRET = "signing_secret_from_dashboard"
async def tabpay_webhook(request: web.Request):
# The v2 signature is computed over the "timestamp.body" string - raw bytes, before parsing 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)
# A stale timestamp means a replay of an intercepted notification
if abs(time.time() - int(timestamp)) > 300:
return web.Response(status=401)
event = json.loads(raw)
# Test notification from the dashboard: acknowledge it, but do not deliver the product
if event.get("test"):
return web.Response(status=200)
if event["status"] == "SUCCESS" and event["telegramId"]:
# Idempotency: mark event["id"] as processed,
# for a repeated webhook with the same id just respond 200
await bot.send_message(
int(event["telegramId"]),
"Payment received - delivering the product.",
)
return web.Response(status=200)
app = web.Application()
app.router.add_post("/tabpay/webhook", tabpay_webhook)
web.run_app(app, port=8080)Test the integration without a payment
The Webhook tab in the shop settings has a Send test webhook button: it sends a notification to your URL, built exactly like a live one with a valid signature, but with test: true (live notifications carry false). The handler must respond 200 and must not deliver the product - as in the examples above. This verifies the signature and notification delivery without a real payment.
Things to keep in mind
- Deliver the product only on
status: "SUCCESS"- any other status (reference) means the money has not been received. - A webhook can arrive more than once (without a 2xx response delivery is retried, up to 7 attempts with increasing delays) - mark the payment as processed by
idand never deliver the product twice. - telegramId is returned as a string - cast it to a number if needed before sending the message.
- If the notification was not received (for example, the bot server was down), verify the payment status with a Retrieve a payment request.
- To bring the buyer back to the bot chat after payment, pass successUrl with an https://t.me/your_bot link when creating the payment - the payment page will redirect the buyer there after a successful payment.
If an AI assistant is doing the integration, hand it the materials from the AI and tools page - the ready-made prompt and llms.txt include this recipe.