Use TON Connect native sendTransaction method when:
The application manages wallet connection UI and transaction flow directly.
TON Connect is already used elsewhere in the application.
Access to TON Connect-specific APIs is required, such as status change listeners or custom wallet adapters.
Integration overview
Direct TON Connect integration follows these steps:
Configure the TON Connect UI provider with the application manifest.
Manage wallet connection state and provide UI for users to connect the wallets.
Create a transaction message on the client or backend using createTonPayTransfer.
Send the transaction using TON Connect’s sendTransaction method.
Observe connection state changes and transaction status updates.
React implementation
Set up application
Wrap the application with the TON Connect UI provider:
import { TonConnectUIProvider } from "@tonconnect/ui-react" ;
export function Providers ({ children } : { children : React . ReactNode }) {
return (
< TonConnectUIProvider manifestUrl = "/tonconnect-manifest.json" >
{ children }
</ TonConnectUIProvider >
);
}
The manifest URL must be publicly accessible and served over HTTPS in production. The manifest file provides application metadata required for wallet identification.
Payment component
import {
useTonAddress ,
useTonConnectModal ,
useTonConnectUI ,
} from "@tonconnect/ui-react" ;
import { createTonPayTransfer } from "@ton-pay/api" ;
import { useState } from "react" ;
export function PaymentComponent ({ orderId , amount } : { orderId : string ; amount : number }) {
const address = useTonAddress ( true ); // Get user-friendly address format
const { open } = useTonConnectModal ();
const [ tonConnectUI ] = useTonConnectUI ();
const [ loading , setLoading ] = useState ( false );
const handlePayment = async () => {
// Check if wallet is connected
if ( ! address ) {
open ();
return ;
}
setLoading ( true );
try {
// Create the transaction message
// Note: For production, consider moving this to a server endpoint
const { message , reference , bodyBase64Hash } = await createTonPayTransfer (
{
amount ,
asset: "TON" ,
recipientAddr: "<RECIPIENT_WALLET_ADDRESS>" ,
senderAddr: address ,
commentToSender: `Payment for Order ${ orderId } ` ,
commentToRecipient: `Order ${ orderId } ` ,
},
{
chain: "testnet" ,
// API key can be used client-side
apiKey: "<TONPAY_API_KEY>" ,
}
);
// Store tracking identifiers
await fetch ( "/api/store-payment" , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({ reference , bodyBase64Hash , orderId , amount }),
});
// Send transaction through TonConnect
const result = await tonConnectUI . sendTransaction ({
messages: [ message ],
validUntil: Math . floor ( Date . now () / 1000 ) + 300 , // 5 minutes
from: address ,
});
console . log ( "Transaction sent:" , result . boc );
// Handle success
window . location . href = `/orders/ ${ orderId } /success` ;
} catch ( error ) {
console . error ( "Payment failed:" , error );
alert ( "Payment failed. Please try again." );
} finally {
setLoading ( false );
}
};
return (
< button onClick = { handlePayment } disabled = { loading } >
{ loading ? "Processing..." : address ? `Pay ${ amount } TON` : "Connect Wallet" }
</ button >
);
}
See all 74 lines
Manage connection state
Listen for wallet connection status changes using the TON Connect UI API:
import { useEffect } from "react" ;
import { useTonConnectUI } from "@tonconnect/ui-react" ;
export function WalletStatus () {
const [ tonConnectUI ] = useTonConnectUI ();
const [ walletInfo , setWalletInfo ] = useState ( null );
useEffect (() => {
// Listen for connection status changes
const unsubscribe = tonConnectUI . onStatusChange (( wallet ) => {
if ( wallet ) {
console . log ( "Wallet connected:" , wallet . account . address );
setWalletInfo ({
address: wallet . account . address ,
chain: wallet . account . chain ,
walletName: wallet . device . appName ,
});
} else {
console . log ( "Wallet disconnected" );
setWalletInfo ( null );
}
});
return () => {
unsubscribe ();
};
}, [ tonConnectUI ]);
if ( ! walletInfo ) {
return < div > No wallet connected </ div > ;
}
return (
< div >
< p > Connected: { walletInfo . walletName } </ p >
< p > Address: { walletInfo . address } </ p >
</ div >
);
}
Server-side message building
For production applications, build transaction messages on the server to centralize tracking and validation.
Backend endpoint
import { createTonPayTransfer } from "@ton-pay/api" ;
import { validateWalletAddress } from "./utils/validation" ;
app . post ( "/api/create-transaction" , async ( req , res ) => {
const { orderId , senderAddr } = req . body ;
try {
// Validate inputs
if ( ! validateWalletAddress ( senderAddr )) {
return res . status ( 400 ). json ({ error: "Invalid wallet address" });
}
// Fetch order details from database
const order = await db . orders . findById ( orderId );
if ( ! order ) {
return res . status ( 404 ). json ({ error: "Order not found" });
}
if ( order . status !== "pending" ) {
return res . status ( 400 ). json ({ error: "Order already processed" });
}
// Create transaction message
const { message , reference , bodyBase64Hash } = await createTonPayTransfer (
{
amount: order . amount ,
asset: order . currency || "TON" ,
recipientAddr: "<RECIPIENT_WALLET_ADDRESS>" ,
senderAddr ,
commentToSender: `Payment for Order ${ order . id } ` ,
commentToRecipient: `Order ${ order . id } - ${ order . description } ` ,
},
{
chain: "testnet" ,
apiKey: "TONPAY_API_KEY" ,
}
);
// Store tracking identifiers
await db . payments . create ({
orderId: order . id ,
reference ,
bodyBase64Hash ,
amount: order . amount ,
asset: order . currency || "TON" ,
senderAddr ,
status: "pending" ,
createdAt: new Date (),
});
// Return message to client
res . json ({ message });
} catch ( error ) {
console . error ( "Failed to create transaction:" , error );
res . status ( 500 ). json ({ error: "Failed to create transaction" });
}
});
See all 57 lines
Frontend implementation
export function ServerManagedPayment ({ orderId } : { orderId : string }) {
const address = useTonAddress ( true );
const { open } = useTonConnectModal ();
const [ tonConnectUI ] = useTonConnectUI ();
const [ loading , setLoading ] = useState ( false );
const handlePayment = async () => {
if ( ! address ) {
open ();
return ;
}
setLoading ( true );
try {
// Request transaction message from server
const response = await fetch ( "/api/create-transaction" , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({ orderId , senderAddr: address }),
});
if ( ! response . ok ) {
const error = await response . json ();
throw new Error ( error . error || "Failed to create transaction" );
}
const { message } = await response . json ();
// Send transaction
const result = await tonConnectUI . sendTransaction ({
messages: [ message ],
validUntil: Math . floor ( Date . now () / 1000 ) + 300 ,
from: address ,
});
console . log ( "Transaction completed:" , result . boc );
// Navigate to success page
window . location . href = `/orders/ ${ orderId } /success` ;
} catch ( error ) {
console . error ( "Payment error:" , error );
alert ( error . message || "Payment failed" );
} finally {
setLoading ( false );
}
};
return (
< button onClick = { handlePayment } disabled = { loading } >
{ loading ? "Processing..." : "Complete Payment" }
</ button >
);
}
See all 54 lines
Vanilla JavaScript implementation
For non-React applications, use the TON Connect SDK directly:
import TonConnectUI from "@tonconnect/ui" ;
import { createTonPayTransfer } from "@ton-pay/api" ;
const tonConnectUI = new TonConnectUI ({
manifestUrl: "https://yourdomain.com/tonconnect-manifest.json" ,
});
// Connect wallet
async function connectWallet () {
await tonConnectUI . connectWallet ();
}
// Send payment
async function sendPayment ( amount , orderId ) {
const wallet = tonConnectUI . wallet ;
if ( ! wallet ) {
await connectWallet ();
return ;
}
try {
// Create transaction message
const { message , reference , bodyBase64Hash } = await createTonPayTransfer (
{
amount ,
asset: "TON" ,
recipientAddr: "<RECIPIENT_WALLET_ADDRESS>" ,
senderAddr: wallet . account . address ,
commentToSender: `Order ${ orderId } ` ,
},
{ chain: "testnet" }
);
// Store tracking data
await fetch ( "/api/store-payment" , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({ reference , bodyBase64Hash , orderId }),
});
// Send transaction
const result = await tonConnectUI . sendTransaction ({
messages: [ message ],
validUntil: Math . floor ( Date . now () / 1000 ) + 300 ,
from: wallet . account . address ,
});
console . log ( "Payment successful:" , result . boc );
} catch ( error ) {
console . error ( "Payment failed:" , error );
}
}
// Listen for connection changes
tonConnectUI . onStatusChange (( wallet ) => {
if ( wallet ) {
console . log ( "Wallet connected:" , wallet . account . address );
document . getElementById ( "wallet-address" ). textContent = wallet . account . address ;
} else {
console . log ( "Wallet disconnected" );
document . getElementById ( "wallet-address" ). textContent = "Not connected" ;
}
});
See all 64 lines
Transaction parameters
Message structure
The message object passed to sendTransaction must include these fields:
Amount to send in nanotons.
Base64-encoded message payload containing transfer details and tracking information.
Transaction options
Unix timestamp indicating when the transaction expires. Typically is 5 minutes. validUntil : Math . floor ( Date . now () / 1000 ) + 300
Sender’s wallet address. Must match the connected wallet address.
Network identifier. Usually omitted as it is inferred from the connected wallet.
Error handling
async function handleTransaction () {
try {
const result = await tonConnectUI . sendTransaction ({
messages: [ message ],
validUntil: Math . floor ( Date . now () / 1000 ) + 300 ,
from: address ,
});
return result ;
} catch ( error ) {
// User rejected the transaction
if ( error . message ?. includes ( "rejected" )) {
console . log ( "User cancelled the transaction" );
return null ;
}
// Wallet not connected
if ( error . message ?. includes ( "Wallet is not connected" )) {
console . log ( "Connect the wallet first" );
tonConnectUI . connectWallet ();
return null ;
}
// Transaction expired
if ( error . message ?. includes ( "expired" )) {
console . log ( "Transaction expired, please try again" );
return null ;
}
// Network or other errors
console . error ( "Transaction failed:" , error );
throw error ;
}
}
Best practices
Check wallet connection status before attempting to send transactions. Provide clear UI feedback for connection state.
const wallet = tonConnectUI . wallet ;
if ( ! wallet ) {
// Show connect button
return ;
}
// Proceed with transaction
Use a reasonable validUntil value, typically 5 minutes, to prevent stale transactions while allowing enough time for user confirmation.
const validUntil = Math . floor ( Date . now () / 1000 ) + 300 ; // 5 minutes
Ensure the sender address from TON Connect matches the format expected by the backend. Use the user-friendly format consistently.
const address = useTonAddress ( true ); // true = user-friendly format
Always persist reference and bodyBase64Hash before sending the transaction. This allows payment reconciliation through webhooks even if the client flow fails after submission.
// Good: Store first, then send
await storePaymentTracking ( reference , bodyBase64Hash );
await tonConnectUI . sendTransaction ( ... );
// Bad: Send first, then store
await tonConnectUI . sendTransaction ( ... );
await storePaymentTracking ( reference , bodyBase64Hash ); // Might not execute
Implement connection state listeners to update the UI and handle wallet disconnections.
useEffect (() => {
const unsubscribe = tonConnectUI . onStatusChange (( wallet ) => {
if ( wallet ) {
setConnectedWallet ( wallet . account . address );
} else {
setConnectedWallet ( null );
}
});
return unsubscribe ;
}, [ tonConnectUI ]);
Handle transaction rejection explicitly. Treat user rejection as a normal cancellation, not as an error.
try {
await tonConnectUI . sendTransaction ( transaction );
} catch ( error ) {
if ( error . message ?. includes ( "rejected" )) {
// Don't show error - user intentionally cancelled
console . log ( "Transaction cancelled by user" );
} else {
// Show error for unexpected failures
showErrorMessage ( "Transaction failed" );
}
}
Troubleshooting
If a transaction fails with Wallet is not connected
Ensure the wallet is connected before calling sendTransaction: if ( ! tonConnectUI . wallet ) {
await tonConnectUI . connectWallet ();
// Wait for connection before proceeding
}
Add a connection state check: const isConnected = tonConnectUI . wallet !== null ;
If sendTransaction throws Invalid address format
If a transaction expires before the user signs it
The validUntil timestamp may be too short. Increase the validity period to give the user more time to confirm the transaction: // Increase from 5 to 10 minutes if needed
validUntil : Math . floor ( Date . now () / 1000 ) + 600
If the manifest URL fails to load
Check for the following common issues:
The URL is not publicly accessible.
CORS headers are not configured correctly.
The manifest JSON is malformed.
The URL is not HTTPS; required in production.
Open the manifest URL directly in a browser to verify that it is accessible.
If onStatusChange is not triggered
Ensure that the status change subscription is created once and remains active for the lifetime of the component. useEffect (() = > {
const unsubscribe = tonConnectUI . onStatusChange ( handleWalletChange );
return () = > unsubscribe (); // Clean up subscription
},
[ tonConnectUI ]);
If multiple wallet connection prompts appear
connectWallet() may be called more than once. Track the connection state:const [ isConnecting , setIsConnecting ] = useState ( false );
const connect = async () = > {
if ( isConnecting ) return;
setIsConnecting ( true );
try {
await tonConnectUI . connectWallet ();
} finally {
setIsConnecting ( false );
}
};
API key configuration
When using TON Connect with server-side message building, the API key can be included in the backend:
import { createTonPayTransfer } from "@ton-pay/api" ;
app . post ( "/api/create-transaction" , async ( req , res ) => {
const { orderId , senderAddr } = req . body ;
const order = await db . orders . findById ( orderId );
const { message , reference , bodyBase64Hash } = await createTonPayTransfer (
{
amount: order . amount ,
asset: "TON" ,
recipientAddr: "<RECIPIENT_WALLET_ADDRESS>" ,
senderAddr: "<SENDER_WALLET_ADDRESS>" ,
},
{
chain: "testnet" ,
apiKey: "TONPAY_API_KEY" ,
}
);
await db . payments . create ({ orderId , reference , bodyBase64Hash });
res . json ({ message });
});
Testnet configuration
Funds at risk Running tests on mainnet can result in irreversible loss of real TON. Always use chain: "testnet" and testnet wallet addresses during development. Verify the network before switching to mainnet.
Set up testnet
Configure the environment to use testnet:
# .env.development
TON_CHAIN = testnet
MERCHANT_WALLET_ADDRESS = TESTNET_ADDRESS
import { useTonAddress , useTonConnectUI } from "@tonconnect/ui-react" ;
import { createTonPayTransfer } from "@ton-pay/api" ;
export function TestnetPayment ({ amount } : { amount : number }) {
const address = useTonAddress ( true );
const [ tonConnectUI ] = useTonConnectUI ();
const handlePayment = async () => {
if ( ! address ) {
tonConnectUI . connectWallet ();
return ;
}
const { message } = await createTonPayTransfer (
{
amount ,
asset: "TON" ,
recipientAddr: "<RECIPIENT_WALLET_ADDRESS>" ,
senderAddr: "<SENDER_WALLET_ADDRESS>" ,
},
{ chain: "testnet" } // Use testnet
);
const result = await tonConnectUI . sendTransaction ({
messages: [ message ],
validUntil: Math . floor ( Date . now () / 1000 ) + 300 ,
from: address ,
});
console . log ( "Testnet transaction:" , result . boc );
};
return < button onClick = { handlePayment } > Test Payment </ button > ;
}
Next steps