TON deep-link
URL scheme `ton://...` for passing operation parameters directly to a TON wallet: recipient address, amount, comment, optionally a payload. The base UX abstraction that unites different wallets.
Aliases: ton deep link, deeplink, deep link
TON deep-link is the URL scheme ton://transfer/<address>?amount=<nano>&text=<message>. When a user clicks such a link, the OS opens the registered TON wallet (Tonkeeper, MyTonWallet, @Wallet, etc.) with the transaction form pre-filled.
Base parameters
amount— in nano-TON (1 TON = 10^9 nano).text— comment (UTF-8, ≤ ~120 bytes).bin— base64 payload for a smart-contract call (jetton transfer, NFT action, etc.).expires— Unix timestamp deadline.
When to use it
- Donation links, QR codes for offline payments, payment requests in chat — the simplest and serverless option.
- An alternative to TON Connect for one-shot operations that don’t need a long-lived authorisation.
Limits
- You can’t get a response back from the wallet in the browser (this is fire-and-forget) — for interactive flows you need TON Connect 2.x.
- On desktop with no TON wallet installed, the link won’t open — the fallback should show a QR code.
Real-world examples
Simple 1 TON transfer with a comment:
ton://transfer/EQAbc...XYZ?amount=1000000000&text=Hello%20TON
Jetton transfer (USDT) — parameters are packed into bin, a base64-url payload containing the serialised transfer#0f8a7ea5 op-code, jetton amount in minor units and the destination address:
import { beginCell, toNano } from '@ton/core';
const payload = beginCell()
.storeUint(0x0f8a7ea5, 32) // op transfer
.storeUint(Date.now(), 64) // query_id
.storeCoins(toNano('10')) // amount
.storeAddress(destination) // recipient
.storeAddress(owner) // response destination
.storeBit(0) // no custom payload
.storeCoins(toNano('0.05')) // forward_ton_amount
.storeBit(0) // forward_payload null
.endCell();
const bin = payload.toBoc().toString('base64url');
const link = `ton://transfer/${jettonWallet}?amount=50000000&bin=${bin}`;
The 50_000_000 nano-TON (~$0.10) are fees for executing the jetton transfer — without them the message would bankrupt the jetton-wallet contract.
Telegram-bot integration
Inside a bot, a deep-link button is most ergonomically shipped via InlineKeyboardButton.url:
const url = `ton://transfer/${address}?amount=${nanoAmount}&text=${encodeURIComponent(memo)}`;
bot.sendMessage(chatId, 'Pay for the order:', {
reply_markup: { inline_keyboard: [[{ text: '💎 Pay', url }]] },
});
Telegram on iOS before 10.5 wouldn’t open arbitrary ton:// schemes — you had to wrap them in https://app.tonkeeper.com/transfer/..., which redirected to the native scheme. Support has been stable since 2024-2025, but a Tonkeeper-bridge URL fallback is still reasonable for legacy clients.
Security and validation
When parsing a deep-link on a site or bot, always:
- Verify the address — the bounceable vs non-bounceable form must match the contract type (jetton-wallets are always bounceable; wallet-V5 accepts either).
- Convert
amountto human-readable before displaying — otherwise the user sees1000000000and doesn’t realise it’s 1 TON. - Sanitise
text— it can carry unicode spoofing (RTL override, zero-width). - Don’t trust
expires— the wallet validates it itself, but in the site UI show “until 15:42”, not the raw timestamp.
Related
- TON Connect — for interactive operations that need a wallet response.
- Jetton — payload format for token transfer.
- Mini-app — embedded UI where deep-links give way to the TON Connect SDK.