← All articles

Accepting USDT in a Telegram Bot with Webhooks

October 10, 2026 · 7 min read

To accept USDT in a Telegram bot, the bot creates a payment invoice on your server, sends the customer a Pay button that opens a hosted payment page, and delivers the product when your server receives a signed “paid” webhook. The customer pays by Binance Pay, TRC20 or BEP20, the money goes straight to your Binance, and the bot confirms the purchase on its own, usually within a minute.

This tutorial builds a complete working example in Python, about 80 lines, using the Clearqo API and the Telegram Bot API. No Telegram library is required.

How the flow works

  1. The user sends /buy to your bot.
  2. Your server creates an invoice with an order_id that includes the user’s chat ID.
  3. The bot replies with the exact amount and a Pay with USDT button linking to checkout_url.
  4. The user pays. Clearqo detects it and sends invoice.paid to your webhook.
  5. Your server verifies the signature, re-fetches the invoice, and the bot delivers: an invite link, a licence key, a file, anything.

What you need

Set these environment variables on the server. Never put them in your code or repository:

export TELEGRAM_TOKEN="123456:ABC..."      # from @BotFather
export TELEGRAM_SECRET="a-long-random-string" # protects your Telegram webhook
export CLEARQO_KEY="cq_live_..."              # Dashboard → Developers
export CLEARQO_WEBHOOK_SECRET="..."           # Dashboard → Developers → Signing secret

The complete bot

import hashlib, hmac, os, sqlite3, threading, time

import requests
from flask import Flask, abort, request

TG = f"https://api.telegram.org/bot{os.environ['TELEGRAM_TOKEN']}"
API = 'https://clearqo.com/wp-json/clearqo/v1'
KEY = {'X-Api-Key': os.environ['CLEARQO_KEY']}
PRICE = '5'  # USDT

app = Flask(__name__)
db = sqlite3.connect('bot.db', check_same_thread=False)
db.execute('CREATE TABLE IF NOT EXISTS delivered (invoice TEXT PRIMARY KEY)')
db_lock = threading.Lock()


def say(chat_id, text, pay_url=None):
    msg = {'chat_id': chat_id, 'text': text}
    if pay_url:
        msg['reply_markup'] = {'inline_keyboard': [[{'text': 'Pay with USDT', 'url': pay_url}]]}
    requests.post(f'{TG}/sendMessage', json=msg, timeout=15)


@app.post('/telegram')
def telegram():
    # Only Telegram knows this secret (set with setWebhook below).
    if request.headers.get('X-Telegram-Bot-Api-Secret-Token') != os.environ['TELEGRAM_SECRET']:
        abort(403)
    message = (request.get_json(silent=True) or {}).get('message') or {}
    chat_id = message.get('chat', {}).get('id')
    if chat_id and message.get('text') == '/buy':
        res = requests.post(f'{API}/invoices', headers=KEY, timeout=15, json={
            'amount': PRICE,
            'order_id': f'tg:{chat_id}:{int(time.time())}',
            'description': 'Premium access - 30 days',
        })
        if res.ok:
            invoice = res.json()
            say(chat_id, f"Send exactly {invoice['amount_to_pay']} USDT. "
                         'The last digits identify your payment.', invoice['checkout_url'])
        else:
            say(chat_id, 'Payments are unavailable right now. Please try again later.')
    return 'ok'


def valid_signature(raw: bytes, header: str) -> bool:
    parts = dict(p.split('=', 1) for p in header.split(',') if '=' in p)
    t = parts.get('t', '')
    if not t.isdigit() or abs(time.time() - int(t)) > 300:
        return False  # missing or older than 5 minutes: possible replay
    expected = hmac.new(os.environ['CLEARQO_WEBHOOK_SECRET'].encode(),
                        f'{t}.'.encode() + raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get('v1', ''))


@app.post('/clearqo')
def clearqo():
    if not valid_signature(request.get_data(), request.headers.get('X-Clearqo-Signature', '')):
        abort(400)
    event = request.get_json()
    if event.get('type') != 'invoice.paid':
        return 'ok'
    # Never trust the webhook body for money: ask the API for the current invoice.
    invoice = requests.get(f"{API}/invoices/{event['data']['id']}", headers=KEY, timeout=15).json()
    if invoice.get('status') != 'paid' or not invoice.get('order_id', '').startswith('tg:'):
        return 'ok'
    chat_id = invoice['order_id'].split(':')[1]
    if float(invoice['paid_price_amount']) < float(invoice['price_amount']):
        say(chat_id, 'We received less than the price. Our team will contact you.')
        return 'ok'
    with db_lock:  # deliver once, even if the webhook arrives twice
        try:
            db.execute('INSERT INTO delivered VALUES (?)', (invoice['id'],))
            db.commit()
        except sqlite3.IntegrityError:
            return 'ok'
    say(chat_id, f"Payment received: {invoice['paid_amount']} USDT. Welcome to Premium! ...")
    return 'ok'

Replace the last say(...) with your real delivery, for example a one-time invite link to a private channel, a licence key, or a file.

Connect the two webhooks

1. Telegram → your bot. Tell Telegram where to send messages, including the secret it must echo back:

curl "https://api.telegram.org/bot$TELEGRAM_TOKEN/setWebhook" \
  -d "url=https://bot.example.com/telegram" \
  -d "secret_token=$TELEGRAM_SECRET"

2. Clearqo → your bot. In Dashboard → Developers, set the Endpoint URL to https://bot.example.com/clearqo, save, and click Send test event. Your server receives a signed test.ping. The code above ignores it but still answers 200, so the dashboard shows the delivery as successful.

Run the app behind your HTTPS web server or a reverse proxy, with a production server such as gunicorn, not Flask’s development server.

Why each security step matters

Why the order ID contains the chat ID

order_id is your own reference, returned in every invoice and webhook. Encoding tg:<chat_id>:<timestamp> lets the webhook find the right user without another database table. Allowed characters are letters, numbers and _ - . : #, up to 100 characters. Group chat IDs are negative numbers, which is fine because - is allowed.

Going further

FAQ

Can users pay without leaving Telegram?

The Pay button opens the payment page in Telegram’s in-app browser. The user copies the amount and Pay ID or address and pays in their Binance app or wallet, then returns to the chat, where the bot has already confirmed.

Does the bot need my Binance keys?

No. The bot only uses your Clearqo API key. Your read-only Binance key is stored, encrypted, in Clearqo, and the money goes straight to your Binance account.

What if my server is down when the payment arrives?

Clearqo retries the webhook after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours. You can also list paid invoices with GET /invoices?status=paid to catch up.

Accept USDT on your website

Paid straight to your own Binance, verified automatically.

Create free account