Принимайте криптоплатежи в чате Telegram
Создайте платёж через API, отправьте клиенту ссылку telegramUrl в вашем боте и подтвердите оплату вебхуком.
Как это работает
- 1Создайте платёж через API. В ответе будет telegramUrl.
- 2Отправьте telegramUrl клиенту в вашем боте.
- 3Клиент платит в боте Qvard @QvardBot, а вы получаете вебхук со статусом.
Создайте платёж
Вызывайте API с вашего сервера и передавайте секретный ключ (qvard_sk_…) в заголовке Authorization: Bearer. В теле три поля:
- price: сумма в валюте currency. По умолчанию USD.
- currency: код валюты ISO-4217, например USD или INR.
- id: ваш номер заказа, до 128 символов. Повторный запрос с тем же id вернёт тот же платёж.
curl -X POST https://api.qvard.com/api/v1/payment \
-H 'Authorization: Bearer qvard_sk_YOUR_SECRET_KEY' \
-H 'Content-Type: application/json' \
-d '{"price":10,"currency":"USD","id":"order-123"}'// Run on your server only: the secret key must not reach the browser
async function createPayment(orderId) {
const res = await fetch('https://api.qvard.com/api/v1/payment', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.QVARD_SECRET_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ price: 10, currency: 'USD', id: orderId })
});
if (!res.ok) throw new Error('Qvard API error: ' + res.status);
const { data } = await res.json();
return data; // { id, redirectUrl, telegramUrl }
}import os
import requests
# Run on your server only: the secret key must not reach the browser
r = requests.post(
"https://api.qvard.com/api/v1/payment",
headers={"Authorization": "Bearer " + os.environ["QVARD_SECRET_KEY"]},
json={"price": 10, "currency": "USD", "id": "order-123"},
timeout=10,
)
r.raise_for_status()
payment = r.json()["data"] # id, redirectUrl, telegramUrl{
"data": {
"id": "…",
"redirectUrl": "https://my.qvard.com/pay/…",
"telegramUrl": "https://t.me/QvardBot?start=pay_…"
},
"code": 201,
"message": "success",
"status": true
}Ответ обёрнут в поле data. telegramUrl (https://t.me/QvardBot?start=pay_…) приходит, когда бот Qvard настроен; redirectUrl есть всегда.
Отправьте ссылку в вашем боте
Пример создаёт платёж по команде /buy, сохраняет ваш номер заказа и отправляет кнопку с telegramUrl. Токен бота и секретный ключ берите из переменных окружения.
import os
import httpx
from aiogram import Bot, Dispatcher
from aiogram.filters import Command
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
bot = Bot(token=os.environ["BOT_TOKEN"])
dp = Dispatcher()
@dp.message(Command("buy"))
async def buy(message: Message):
order_id = f"order-{message.message_id}" # your own order ID
async with httpx.AsyncClient(timeout=10) as http:
r = await http.post(
"https://api.qvard.com/api/v1/payment",
headers={"Authorization": "Bearer " + os.environ["QVARD_SECRET_KEY"]},
json={"price": 10, "currency": "USD", "id": order_id},
)
r.raise_for_status()
telegram_url = r.json()["data"]["telegramUrl"]
# save order_id and message.chat.id in your database here
kb = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Pay", url=telegram_url)]])
await message.answer("Pay the order in the Qvard bot:", reply_markup=kb)
if __name__ == "__main__":
import asyncio
asyncio.run(dp.start_polling(bot))import { Bot, InlineKeyboard } from 'grammy';
const bot = new Bot(process.env.BOT_TOKEN);
bot.command('buy', async (ctx) => {
const orderId = 'order-' + ctx.msg.message_id; // your own order ID
const res = await fetch('https://api.qvard.com/api/v1/payment', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.QVARD_SECRET_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ price: 10, currency: 'USD', id: orderId })
});
if (!res.ok) throw new Error('Qvard API error: ' + res.status);
const { data } = await res.json();
// save orderId and ctx.chat.id in your database here
await ctx.reply('Pay the order in the Qvard bot:', {
reply_markup: new InlineKeyboard().url('Pay', data.telegramUrl)
});
});
bot.start();Подтвердите платёж
Qvard отправляет { id, status } на адрес вашего вебхука, подпись в заголовке X-Qvard-Signature. Сначала проверьте подпись по сырому телу, как описано на странице вебхука. Потом пометьте заказ оплаченным и напишите клиенту в вашем боте. Действуйте по статусу completed; partial значит, что пришла только часть суммы.
const crypto = require('crypto');
const express = require('express');
function verifyQvardSignature(rawBody, header, secret, toleranceSec = 300) {
const parts = header.split(',').map((p) => p.trim());
const t = Number((parts.find((p) => p.startsWith('t=')) || '').slice(2));
const sigs = parts.filter((p) => p.startsWith('v1=')).map((p) => p.slice(3));
if (!t || sigs.length === 0) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest();
return sigs.some((s) => {
const got = Buffer.from(s, 'hex');
return got.length === expected.length && crypto.timingSafeEqual(got, expected);
});
}
// Use the RAW body: express.raw, not express.json
app.post('/qvard-webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const raw = req.body.toString('utf8');
if (!verifyQvardSignature(raw, req.get('X-Qvard-Signature') || '', process.env.QVARD_SECRET_KEY)) {
return res.sendStatus(400);
}
const { id, status } = JSON.parse(raw); // id is your order ID
if (status === 'completed') {
// check the amount against your order, then mark it paid; markPaid is your own function
const order = await markPaid(id); // returns { chatId } of the order
await bot.api.sendMessage(order.chatId, 'Payment received. Thank you!'); // bot from the grammY example
}
res.sendStatus(200);
});import hashlib
import hmac
import json
import os
import time
from aiogram import Bot # the bot from the aiogram example
from fastapi import FastAPI, Request, Response
app = FastAPI()
def verify_qvard_signature(raw: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = [p.strip() for p in header.split(",")]
t = next((p[2:] for p in parts if p.startswith("t=")), "")
sigs = [p[3:] for p in parts if p.startswith("v1=")]
if not t.isdigit() or not sigs or abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + raw, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s) for s in sigs)
@app.post("/qvard-webhook")
async def qvard_webhook(request: Request):
raw = await request.body() # raw bytes, before any JSON parsing
if not verify_qvard_signature(raw, request.headers.get("X-Qvard-Signature", ""), os.environ["QVARD_SECRET_KEY"]):
return Response(status_code=400)
event = json.loads(raw) # {"id": ..., "status": ...}; id is your order ID
if event["status"] == "completed":
# check the amount against your order, then mark it paid; mark_paid is your own function
chat_id = mark_paid(event["id"]) # returns the chat_id of the order
await bot.send_message(chat_id, "Payment received. Thank you!")
return Response(status_code=200)Что видит клиент
- 1Клиент открывает ссылку telegramUrl. Бот показывает сумму и предлагает выбрать актив и сеть.
- 2После выбора бот показывает адрес (нажатием его можно скопировать), сумму, время до конца счёта и кнопки «QR», «Обновить статус» и «Открыть страницу оплаты».
- 3Статус обновляется в том же сообщении.
- 4При полной оплате бот присылает отдельное сообщение «Платёж получен» (оно даёт push на телефон), при частичной — отдельное сообщение «получено X, осталось Y».
Правила Telegram
Telegram требует оплату Telegram Stars за цифровые товары и услуги, которые продаются внутри ботов и Mini App, и не допускает для них криптовалюту. Бот Qvard подходит для физических товаров и для платежей, которые не являются цифровыми товарами внутри Telegram. Соблюдать правила Telegram должен продавец.
Ограничения
- Бот Qvard @QvardBot общий для всех мерчантов. Подключить свой бот нельзя.
- Telegram Stars и Mini App пока не поддерживаются.
- Уведомления операторам в Telegram пока не реализованы.