Accept TON and USDT payments in a Telegram bot or mini app
If you sell inside Telegram — a bot with a paid tier, a mini app with in-game items, a channel with paid access — your customers already have a TON wallet one tap away. This post shows how to take those payments with easyway: your bot creates an invoice, sends a pay button, and unlocks the order when a signed webhook says it is paid. It also covers the one thing that is different on TON, the memo.
What a Telegram customer can pay with
easyway accepts 6 assets on TON. All of them need 1 confirmation, so a payment is final within seconds. You can offer all of them or limit an invoice to the ones you want, for example only USDT so that the price does not move.
| Asset | API id | Confirmations | Min. withdrawal | Withdrawal fee |
|---|---|---|---|---|
| USDT (Tether) | USDT_TON | 1 | 0.5 USDT | 0.1 USDT |
| GRAM (Gram) | GRAM | 1 | 0.35 GRAM | 0.02 GRAM |
| NOT (Notcoin) | NOT_TON | 1 | 1000 NOT | 400 NOT |
| DOGS (DOGS) | DOGS_TON | 1 | 10000 DOGS | 4000 DOGS |
| CATI (Catizen) | CATI_TON | 1 | 8 CATI | 3 CATI |
| HMSTR (Hamster Kombat) | HMSTR_TON | 1 | 3500 HMSTR | 1300 HMSTR |
Gram is the coin of the TON network (it was called Toncoin before its 2026 rename). The others are jettons, TON's tokens. Every payment pays the same 0.5% platform fee. For example, a customer who pays 20 USDT leaves 19.9 USDT on your balance. Details per coin: USDT on TON and Gram & TON jettons.
The one TON difference: a memo per invoice
On most networks every invoice gets its own deposit address. On TON, all invoices are paid to one shared address, and each invoice has its own numeric memo (the comment of a TON transfer). The memo is what matches the payment to the order. The checkout shows the address, the amount and the memo, each with a copy button. Its QR code is a ton://transfer link that fills in all three in wallets such as Tonkeeper and Telegram Wallet. A transfer without the memo, or with the wrong one, is not credited to any invoice. It is logged as unmatched, and the customer has to contact you. That is why the hosted checkout, which puts the memo in the QR, is the safer choice. If you build your own payment screen, show pay_memo next to pay_address every time it is not null.
Step 1: create the invoice from your bot's backend
The API key stays on your server, never in the bot's client code or the mini app. When a user taps "Buy", your backend creates an invoice. Price it in USD and restrict allowed_assets to TON assets, and the customer picks the coin on the payment page, where the crypto amount is locked for the invoice's lifetime (5 to 1,440 minutes). Put something into order_id that lets you find the order again, such as the Telegram user id plus your order number:
curl -X POST https://api.easyway.cash/v1/invoices \
-H "Authorization: Bearer $EASYWAY_API_KEY" \
-H "Idempotency-Key: tg-51234567-0042" \
-H "Content-Type: application/json" \
-d '{"amount":"20.00","currency":"USD","allowed_assets":["USDT_TON","GRAM"],"order_id":"tg-51234567-0042","description":"Premium, 30 days","lifetime":30}'The answer contains the invoice id and a payment_url. Save the id with the order. The same call in Node.js, PHP and Python is in the API docs.
Step 2: send a pay button
In a bot, send the payment_url as an inline URL button. Telegram opens it in its browser, and the customer pays from their wallet. Here is the whole flow with plain HTTP calls to both APIs:
// Node.js 18+, no libraries. BOT_TOKEN = your bot, EASYWAY_API_KEY = an invoices:write key.
async function sendPayButton(chatId, orderId) {
const res = await fetch("https://api.easyway.cash/v1/invoices", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.EASYWAY_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": orderId, // a retry never makes a second invoice
},
body: JSON.stringify({ amount: "20.00", currency: "USD", allowed_assets: ["USDT_TON", "GRAM"], order_id: orderId, lifetime: 30 }),
});
if (!res.ok) throw new Error(`invoice failed: ${res.status}`);
const invoice = await res.json();
await db.saveOrder(orderId, { chatId, invoiceId: invoice.id }); // remember which invoice is this order's
await fetch(`https://api.telegram.org/bot${process.env.BOT_TOKEN}/sendMessage`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: chatId,
text: "Premium, 30 days: $20. Pay in USDT or Gram on TON.",
reply_markup: { inline_keyboard: [[{ text: "Pay with crypto", url: invoice.payment_url }]] },
}),
});
}In a mini app, call your backend from the app, then open the returned payment_url as a link (for example with the Web App's openLink method). Do not create invoices from the mini app's JavaScript, since anything in there is visible to the user.
Step 3: unlock the order on the webhook
When the payment is seen you get invoice.confirming (you can tell the user "payment detected"). When it is final you get invoice.paid: that is the only event that means money. Check the X-Webhook-Signature header first (how), check that the invoice id is the one you saved for that order, and only then unlock the item and message the user. If someone sends too little, you get invoice.underpaid and the payment page asks for the rest (underpaid and late payments). An invoice that expires is still watched for 7 more days, so a late payment can still arrive as invoice.paid. Before going live, run the whole flow with test keys in the sandbox.
Where the money goes
Each payment is credited to your balance in the asset the customer sent. easyway holds that balance until you withdraw it to an address you have whitelisted with two-factor authentication. TON withdrawals go out as the same asset on TON, with an optional memo for exchange deposit addresses that need one. Nothing is converted. If you do not want to hold Gram or the jettons, accept only USDT_TON.
What this is not
easyway is not a Telegram payment provider. There is no Telegram Stars support, no payment inside the chat window and no ready-made bot or plugin: your bot sends a link, and the customer pays on easyway's checkout page. The same invoices work from a website too, see accepting crypto on a website.