Accept crypto payments in a Telegram chat
Create a payment through the API, send the telegramUrl link to your customer in your bot, and confirm the payment by webhook.
How it works
- 1Create a payment through the API. The response contains telegramUrl.
- 2Send telegramUrl to the customer in your bot.
- 3The customer pays in the Qvard bot @QvardBot, and you receive the webhook with the status.
Create a payment
Call the API from your server and send your secret key (qvard_sk_…) in the Authorization: Bearer header. The body has three fields:
- price: the amount in currency. USD by default.
- currency: ISO-4217 code, for example USD or INR.
- id: your own order ID, up to 128 characters. Sending the same id again returns the same payment.
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
}The answer is wrapped in data. telegramUrl (https://t.me/QvardBot?start=pay_…) is returned when the Qvard bot is set up; redirectUrl is always there.
Send the link in your bot
The example creates a payment for the /buy command, saves your order ID and sends a button with telegramUrl. The bot token and the secret key come from environment variables.
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();Confirm the payment
Qvard posts { id, status } to your webhook URL, signed with X-Qvard-Signature. Check the signature on the raw body first, as described on the webhook page. Then mark the order paid and write to the customer in your bot. Act on status completed; partial means only part of the sum arrived.
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)How to verify the webhook signature
What the customer sees
- 1The customer opens the telegramUrl link. The bot shows the amount and offers to choose an asset and a network.
- 2After the choice, the bot shows the address (tap it to copy), the amount, the time left until the invoice expires, and the buttons QR, Refresh status and Open payment page.
- 3The status updates in the same message.
- 4When the payment is complete, the bot sends a separate message "Payment received", which gives a push notification on the phone. If the payment is partial, it sends a separate message with the amount received and the amount left.
Telegram rules
Telegram requires Telegram Stars for digital goods and services sold inside Telegram bots and Mini Apps, and does not allow crypto for them. The Qvard bot fits physical goods and payments that are not digital goods sold inside Telegram. You are responsible for following Telegram's rules.
Limits
- The Qvard bot @QvardBot is shared by all merchants. You cannot connect your own bot.
- Telegram Stars and Mini Apps are not supported yet.
- Notifications to your operators in Telegram are not implemented yet.