The useTonPay hook provides a React interface for creating TON Pay transfers. It integrates with TON Connect to connect a wallet, sign transactions, and surface transfer errors through the hook state.
How useTonPay works
The useTonPay hook manages the client-side flow for sending a TON Pay transfer. It performs the following steps:
Checks whether a wallet is connected and, if not, opens the TON Connect modal and waits for a successful connection.
Calls an application-provided factory function that builds the transaction message, either in the client-side or by requesting it from a backend service.
Sends the transaction message to the connected wallet for user approval and signing.
Returns the transaction result along with tracking identifiers that can be used for reconciliation.
Integration approaches
useTonPay can be integrated in two ways, depending on where the transaction message is created.
Client-side message building
Message construction happens in the browser.
All transaction fields are provided by the client.
Server-side message building
Message construction happens on the backend.
Tracking identifiers are stored on the server before the message is returned to the client for signing.
Client-side implementation
Prerequisites
The application must be wrapped with the TON Connect UI provider:
import { TonConnectUIProvider } from "@tonconnect/ui-react" ;
export function App () {
return (
< TonConnectUIProvider manifestUrl = "/tonconnect-manifest.json" >
{ /* Application components */ }
</ TonConnectUIProvider >
);
}
The TON Connect manifest file must be publicly accessible and include valid application metadata.
Basic implementation
import { useTonPay } from "@ton-pay/ui-react" ;
import { createTonPayTransfer } from "@ton-pay/api" ;
export function PaymentButton () {
const { pay } = useTonPay ();
const handlePayment = async () => {
try {
const { txResult , message , reference , bodyBase64Hash } = await pay (
async ( senderAddr : string ) => {
const result = await createTonPayTransfer (
{
amount: 3.5 ,
asset: "TON" ,
recipientAddr: "<RECIPIENT_WALLET_ADDRESS>" ,
senderAddr ,
commentToSender: "Payment for Order #8451" ,
},
{
chain: "testnet" ,
// API key can be shown on the client-side, but the secret key never
apiKey: "<TONPAY_API_KEY>" ,
}
);
return {
message: result . message ,
reference: result . reference ,
bodyBase64Hash: result . bodyBase64Hash ,
};
}
);
console . log ( "Transaction completed:" , txResult . boc );
console . log ( "Reference for tracking:" , reference );
console . log ( "Body hash:" , bodyBase64Hash );
// Store tracking identifiers in the database
await savePaymentRecord ({
reference ,
bodyBase64Hash ,
amount: 3.5 ,
asset: "TON" ,
});
} catch ( error ) {
console . error ( "Payment failed:" , error );
// Handle error appropriately
}
};
return < button onClick = { handlePayment } > Pay 3.5 TON </ button > ;
}
See all 52 lines
Response fields
The pay function returns the following fields:
txResult
SendTransactionResponse
required
Transaction result returned by TON Connect. It contains the signed transaction bag of cells and additional transaction details.
The transaction message that was sent, including the recipient address, amount, and payload.
Unique tracking identifier for this transaction. Use it to correlate webhook notifications with orders. Store the reference after creation. It is required to match incoming webhook notifications to specific orders.
Base64-encoded hash of the transaction body. Can be used for advanced transaction verification.
Server-side implementation
Backend endpoint
Create an API endpoint that builds the transaction message and stores tracking data:
import { createTonPayTransfer } from "@ton-pay/api" ;
app . post ( "/api/create-payment" , async ( req , res ) => {
const { amount , senderAddr , orderId } = req . body ;
try {
const { message , reference , bodyBase64Hash } = await createTonPayTransfer (
{
amount ,
asset: "TON" ,
recipientAddr: "<MERCHANT_WALLET_ADDRESS>" ,
senderAddr ,
commentToSender: `Payment for Order ${ orderId } ` ,
commentToRecipient: `Order ${ orderId } ` ,
},
{
chain: "testnet" ,
apiKey: "yourTonPayApiKey" ,
}
);
// Store tracking identifiers in your database
await db . createPayment ({
orderId ,
reference ,
bodyBase64Hash ,
amount ,
asset: "TON" ,
status: "pending" ,
senderAddr ,
});
// Return only the message to the client
res . json ({ message });
} catch ( error ) {
console . error ( "Failed to create payment:" , error );
res . status ( 500 ). json ({ error: "Failed to create payment" });
}
});
See all 39 lines
Persist tracking identifiers before responding Always persist tracking identifiers such as reference and bodyBase64Hash in the database before returning the message to the client. If the client loses connection or closes the browser, these identifiers are still required to process incoming webhooks.
Frontend implementation
import { useTonPay } from "@ton-pay/ui-react" ;
export function ServerPaymentButton ({
orderId ,
amount ,
} : {
orderId : string ;
amount : number ;
}) {
const { pay } = useTonPay ();
const handlePayment = async () => {
try {
const { txResult } = await pay ( async ( senderAddr : string ) => {
const response = await fetch ( "/api/create-payment" , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({ amount , senderAddr , orderId }),
});
if ( ! response . ok ) {
throw new Error ( "Failed to create payment" );
}
const { message } = await response . json ();
return { message };
});
console . log ( "Transaction sent:" , txResult . boc );
// Optionally redirect or show success message
window . location . href = `/orders/ ${ orderId } /success` ;
} catch ( error ) {
console . error ( "Payment failed:" , error );
alert ( "Payment failed. Please try again." );
}
};
return < button onClick = { handlePayment } > Pay { amount } TON </ button > ;
}
See all 40 lines
Error handling
The useTonPay hook throws errors in the following scenarios:
Wallet connection errors
const { pay } = useTonPay ();
try {
await pay ( getMessage );
} catch ( error ) {
if ( error . message === "Wallet connection modal closed" ) {
// User closed the connection modal without connecting
console . log ( "User cancelled wallet connection" );
} else if ( error . message === "Wallet connection timeout" ) {
// Connection attempt exceeded 5-minute timeout
console . log ( "Connection timeout - please try again" );
}
}
Transaction errors
try {
await pay ( getMessage );
} catch ( error ) {
// User rejected the transaction in their wallet
if ( error . message ?. includes ( "rejected" )) {
console . log ( "User rejected the transaction" );
}
// Network or API errors
else if ( error . message ?. includes ( "Failed to create TON Pay transfer" )) {
console . log ( "API error - check your configuration" );
}
// Other unexpected errors
else {
console . error ( "Unexpected error:" , error );
}
}
Best practices
Persist tracking identifiers immediately.
// Good: Store before transaction
const { message , reference } = await createTonPayTransfer ( ... );
await db . createPayment ({ reference , status: "pending" });
return { message };
// Bad: Only storing after successful transaction
const { txResult , reference } = await pay ( ... );
await db . createPayment ({ reference }); // Too late if network fails
Always validate or generate amounts server-side to prevent manipulation. Never trust amount values sent from the client.
// Server-side endpoint
app . post ( '/api/create-payment' , async ( req , res ) => {
const { orderId , senderAddr } = req . body ;
// Fetch the actual amount from your database
const order = await db . getOrder ( orderId );
// Use the verified amount, not req.body.amount
const { message } = await createTonPayTransfer ({
amount: order . amount ,
asset: order . currency ,
recipientAddr: '<RECIPIENT_WALLET_ADDRESS>' ,
senderAddr ,
});
res . json ({ message });
});
Wrap the payment components with React error boundaries to handle failures.
class PaymentErrorBoundary extends React . Component {
componentDidCatch ( error : Error ) {
console . error ( 'Payment component error:' , error );
// Log to the error tracking service
}
render () {
return this . props . children ;
}
}
Payment processing can take several seconds. Provide clear feedback to users during wallet connection and transaction signing.
const [ loading , setLoading ] = useState ( false );
const handlePayment = async () => {
setLoading ( true );
try {
await pay ( getMessage );
} finally {
setLoading ( false );
}
};
return (
< button onClick = { handlePayment } disabled = { loading } >
{ loading ? 'Processing...' : 'Pay Now' }
</ button >
);
Use environment variables for sensitive data and chain configuration.
const { message } = await createTonPayTransfer ( params , {
chain: process . env . TON_CHAIN as 'mainnet' | 'testnet' ,
apiKey: process . env . TONPAY_API_KEY ,
});
Troubleshooting
If the hook throws TonConnect provider not found
Wrap the application with TonConnectUIProvider: import { TonConnectUIProvider } from "@tonconnect/ui-react" ;
function App () {
return (
< TonConnectUIProvider manifestUrl = "/tonconnect-manifest.json" >
< YourComponents />
</ TonConnectUIProvider >
);
}
If the wallet connection modal does not open
Verify that the TON Connect manifest URL is accessible and valid.
Check the browser console for TON Connect initialization errors.
Ensure the manifest file is served with correct CORS headers.
Open the manifest URL directly in the browser to verify it loads.
If the transaction fails with Invalid recipient address
Verify that the recipient address is a valid TON address .
Ensure the address format matches the selected chain; mainnet or testnet.
Verify that the address includes the full base64 representation with workchain.
If the factory function throws Failed to create TON Pay transfer
Check whether the API key is missing or invalid for endpoints that require authentication.
Verify network connectivity.
Validate parameter values. For example, non-negative amounts and valid addresses.
Confirm the correct chain configuration; mainnet or testnet.
To inspect the underlying API error: try {
await createTonPayTransfer ( ... );
} catch ( error ) {
console . error ( "API Error:" , error . cause ); // HTTP status text
}
If the user rejects the transaction and no error is reported
Note that TON Connect may not emit an explicit error on rejection.
Add timeout handling:
const paymentPromise = pay ( getMessage );
const timeoutPromise = new Promise (( _ , reject ) =>
setTimeout (() => reject ( new Error ( "Transaction timeout" )), 60000 )
);
await Promise . race ([ paymentPromise , timeoutPromise ]);
API key configuration
When using useTonPay with server-side message building, the API key can be included in the backend endpoint:
// Backend endpoint
app . post ( "/api/create-payment" , async ( req , res ) => {
const { amount , senderAddr , orderId } = req . body ;
const { message , reference , bodyBase64Hash } = await createTonPayTransfer (
{
amount ,
asset: "TON" ,
recipientAddr: process . env . MERCHANT_WALLET_ADDRESS ! ,
senderAddr ,
},
{
chain: "testnet" ,
apiKey: process . env . TONPAY_API_KEY , // Enables dashboard features
}
);
await db . createPayment ({ 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.
A complete description of testnet configuration, including obtaining testnet TON, testing jetton transfers, verifying transactions, and preparing for mainnet deployment, is available in the Testnet configuration section.
Next steps
Webhook integration Receive real-time notifications when payments complete.
Transaction status Query payment status using reference or body hash.