Use TonConnect’s native sendTransaction method when you need full control over the wallet connection UI and transaction flow. This approach is ideal for applications that require custom UI/UX or need to integrate with existing TonConnect implementations.
When to Use Direct TonConnect
Choose direct TonConnect integration when you need:
Custom UI Control : Full control over wallet connection buttons, modals, and transaction flow presentation
Existing TonConnect Setup : Your application already uses TonConnect for other blockchain interactions
Advanced Features : Access to TonConnect’s advanced features like status change listeners and custom wallet adapters
Non-React Applications : Integration with Vue, Angular, Svelte, or vanilla JavaScript
If you’re building a React application without specific UI requirements, consider using the useTonPay hook for a simpler integration experience.
Integration Overview
Direct TonConnect integration follows this flow:
Provider Setup
Configure the TonConnect UI provider with your application manifest.
Connection Management
Handle wallet connection state and provide UI for users to connect their wallets.
Message Creation
Build the transaction message using createTonPayTransfer on your server or client.
Transaction Sending
Send the transaction through TonConnect’s sendTransaction method.
Status Monitoring
Listen for transaction status updates and connection state changes.
React Implementation
Application Setup
Wrap your application with the TonConnect 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 contains your application’s metadata and is 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: "EQ....yourWalletAddress" ,
senderAddr: address ,
commentToSender: `Payment for Order ${ orderId } ` ,
commentToRecipient: `Order ${ orderId } ` ,
},
{
chain: "testnet" ,
apiKey: "yourTonPayApiKey" ,
}
);
// 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 >
);
}
Connection State Management
Listen for wallet connection changes to update your UI:
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 your 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: "EQ....yourWalletAddress" ,
senderAddr ,
commentToSender: `Payment for Order ${ order . id } ` ,
commentToRecipient: `Order ${ order . id } - ${ order . description } ` ,
},
{
chain: "testnet" ,
apiKey: "yourTonPayApiKey" ,
}
);
// 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" });
}
});
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 >
);
}
Vanilla JavaScript Implementation
For non-React applications, use the TonConnect 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: "EQC...RECIPIENT" ,
senderAddr: wallet . account . address ,
commentToSender: `Order ${ orderId } ` ,
},
{ chain: "mainnet" }
);
// 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" ;
}
});
Transaction Parameters
Message Structure
The message object passed to sendTransaction must include:
Recipient wallet address in user-friendly format (e.g., “EQC…”).
Amount to send in nanotons. For TON payments, multiply the human-readable amount by 1e9.
Base64-encoded message payload containing transfer details and tracking information.
Transaction Options
Unix timestamp indicating when the transaction expires. Typically set to 5 minutes from now. validUntil : Math . floor ( Date . now () / 1000 ) + 300
Sender’s wallet address. Must match the connected wallet address.
Network identifier. Usually omitted as it’s inferred from the connected wallet.
Error Handling
Handle common error scenarios appropriately:
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 ( "Please connect your 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
Always validate wallet connection
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
Set appropriate transaction expiry
Use a reasonable validUntil value (typically 5 minutes) to ensure transactions don’t become stale, but give users enough time to review and sign. const validUntil = Math . floor ( Date . now () / 1000 ) + 300 ; // 5 minutes
Verify address format matches
Store tracking identifiers before transaction
Always persist reference and bodyBase64Hash before sending the transaction. If the transaction succeeds but your code fails afterward, you’ll still be able to reconcile the payment via webhooks. // 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
Monitor wallet connection changes to update your UI and handle disconnections gracefully. useEffect (() => {
const unsubscribe = tonConnectUI . onStatusChange (( wallet ) => {
if ( wallet ) {
setConnectedWallet ( wallet . account . address );
} else {
setConnectedWallet ( null );
}
});
return unsubscribe ;
}, [ tonConnectUI ]);
Handle transaction rejection gracefully
Users may reject transactions in their wallet. Treat rejection as a normal cancellation, not an error. try {
await tonConnectUI . sendTransaction ( ... );
} 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
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
}
Consider adding a connection state check: const isConnected = tonConnectUI . wallet !== null ;
sendTransaction throws 'Invalid address format'
Transaction expires before user signs
The validUntil timestamp is too short or the user is taking too long to sign. Increase the validity period: // Increase from 5 to 10 minutes if needed
validUntil : Math . floor ( Date . now () / 1000 ) + 600
Common manifest issues:
URL is not publicly accessible
CORS headers are not configured correctly
Manifest JSON is malformed
URL is not HTTPS (required in production)
Test your manifest URL directly in a browser to verify accessibility.
onStatusChange not triggering
Ensure you’re properly subscribing to status changes and that the subscription persists: useEffect (() => {
const unsubscribe = tonConnectUI . onStatusChange ( handleWalletChange );
return () => unsubscribe (); // Clean up subscription
}, [ tonConnectUI ]);
Multiple wallet connection prompts
This can happen if you’re calling connectWallet() multiple times. Implement connection state tracking: const [ isConnecting , setIsConnecting ] = useState ( false );
const connect = async () => {
if ( isConnecting ) return ;
setIsConnecting ( true );
try {
await tonConnectUI . connectWallet ();
} finally {
setIsConnecting ( false );
}
};
API Key Configuration
The TON Pay API key is optional but unlocks essential merchant features including transaction tracking, webhook notifications, and wallet management.
When using TonConnect with server-side message building, include the API key in your 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: "EQ....yourWalletAddress" ,
senderAddr ,
},
{
chain: "testnet" ,
apiKey: "yourTonPayApiKey" ,
}
);
await db . payments . create ({ orderId , reference , bodyBase64Hash });
res . json ({ message });
});
For complete details about API key benefits, security best practices, and configuration, see the API Key Configuration section.
Testnet Configuration
Test your TonConnect integration on the TON testnet before production deployment.
Basic Testnet Setup
Configure environment variables for testnet:
# .env.development
TON_CHAIN = testnet
MERCHANT_WALLET_ADDRESS = EQC...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: "EQ....yourWalletAddress" ,
senderAddr: 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 > ;
}
For complete testnet configuration including faucet access, jetton testing, block explorers, webhook testing, best practices, and production transition checklist, see the Testnet Configuration section.
Next Steps