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
- The user sends
/buyto your bot. - Your server creates an invoice with an
order_idthat includes the user’s chat ID. - The bot replies with the exact amount and a Pay with USDT button linking to
checkout_url. - The user pays. Clearqo detects it and sends
invoice.paidto your webhook. - 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
- A bot token from @BotFather.
- A Clearqo account with Binance connected and an API key (Dashboard → Developers).
- A server with HTTPS, because both Telegram and Clearqo deliver webhooks only to
https://URLs. - Python 3.9+ with
pip install flask requests.
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
- Telegram secret token. Without it, anyone who finds
/telegramcould post fake messages to your bot. - Webhook signature and 5-minute window. This proves the event came from Clearqo and wasn’t replayed later. The signature is HMAC-SHA256 of
timestamp + "." + raw body. - Re-fetching the invoice. The API is the single source of truth. Your code acts on what
GET /invoices/{id}returns, not on the event body. - Checking the amount. If you approve an under-payment in the dashboard, the invoice becomes
paidwith less money. Comparepaid_price_amountwithprice_amountbefore delivering. - Deliver once. Webhooks are retried and can arrive twice. The primary key on
deliveredmakes delivery idempotent.
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
- Several products: add
/buy_basic,/buy_proand so on, each with its own price, and put the product code in theorder_id. - Prices in local currency: send
"currency": "PKR"(or any currency you set a rate for) with the amount. See local-currency pricing. - Expiry: invoices expire after 30 minutes by default. Pass
expires_in(10–1440 minutes) to change it, and handleinvoice.expiredto tell the user. - Discord, WhatsApp and others: the same pattern works for any bot. Create an invoice, send the link, deliver on the verified webhook.
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.