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

  1. 1
    Create a payment through the API. The response contains telegramUrl.
  2. 2
    Send telegramUrl to the customer in your bot.
  3. 3
    The 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"}'
{
  "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.

API reference

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))

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);
});

How to verify the webhook signature

What the customer sees

  1. 1
    The customer opens the telegramUrl link. The bot shows the amount and offers to choose an asset and a network.
  2. 2
    After 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.
  3. 3
    The status updates in the same message.
  4. 4
    When 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.

Ready to accept payments in Telegram?

Create an account